Slide 1

Slide 1 text

Testing By Design 1

Slide 2

Slide 2 text

Kostia Tarasenko Hannes Dorfmann 2

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

How to write maintainable tests and how proper architecture can help you 4

Slide 5

Slide 5 text

Unit tests White box testing of individual software components in isolation 5

Slide 6

Slide 6 text

Integration tests “Narrow” definition: Testing the code that connects different components 6

Slide 7

Slide 7 text

Functional tests Black box testing. Feeding input and examining the output 7

Slide 8

Slide 8 text

End-to-end tests Functional tests backed by real environment 8

Slide 9

Slide 9 text

9

Slide 10

Slide 10 text

10

Slide 11

Slide 11 text

Why bother? Functional tests cover integration and unit tests scope 11

Slide 12

Slide 12 text

Challenges - Isolation of side effects - Unreliable slow layers: network, disk access, db access - Every code change affects tests 12

Slide 13

Slide 13 text

What if? … writing and maintaining functional tests was easier? 13

Slide 14

Slide 14 text

14

Slide 15

Slide 15 text

Uncle Bob says: Keep the test code to the same level of quality as the production code. Test code is not throw-away code. 15

Slide 16

Slide 16 text

@Test fun newTodoItemIsShownInTodoList() { onView(withId(R.id.newItem)).perform(click()) onView(withId(R.id.step1Title)).perform(typeText("Another Item")) onView(withId(R.id.button)).perform(click()) onView(withId(R.id.create)).perform(click()) onView(withText("Another Item")).check(matches(isDisplayed())) } 16

Slide 17

Slide 17 text

Write tests yo mama would be proud of 17

Slide 18

Slide 18 text

Robot pattern - influenced by Martin Fowler’s PageObject pattern - Embraced by kotlin DSL syntax 18

Slide 19

Slide 19 text

Robot pattern. Key principles - Let robot do what user can do - Verify what user would see 19

Slide 20

Slide 20 text

20

Slide 21

Slide 21 text

@Test fun newTodoItemIsShownInTodoList() { } 21

Slide 22

Slide 22 text

@Test fun newTodoItemIsShownInTodoList() { todoList { clickCreateTodoItem() } } 22

Slide 23

Slide 23 text

@Test fun newTodoItemIsShownInTodoList() { todoList { clickCreateTodoItem() createItem { } } } 23

Slide 24

Slide 24 text

@Test fun newTodoItemIsShownInTodoList() { todoList { clickCreateTodoItem() createItem { enterTitle("Another Item") } } } 24

Slide 25

Slide 25 text

@Test fun newTodoItemIsShownInTodoList() { todoList { clickCreateTodoItem() createItem { enterTitle("Another Item") pressNext() } } } 25

Slide 26

Slide 26 text

@Test fun newTodoItemIsShownInTodoList() { todoList { clickCreateTodoItem() createItem { enterTitle("Another Item") pressNext() pressSave() } } } 26

Slide 27

Slide 27 text

Robot pattern. Advantages - No need to change tests if the flow doesn’t change - Fun to read - Easy to extend 27

Slide 28

Slide 28 text

Problems with “traditional” Robot Pattern - In which state is the Robot starting? - Assertions hidden - Doesn’t read like a specification 28

Slide 29

Slide 29 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) todoList { } 29

Slide 30

Slide 30 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { todoList { } } 30

Slide 31

Slide 31 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { } } 31

Slide 32

Slide 32 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState() } } 32

Slide 33

Slide 33 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState } } 33

Slide 34

Slide 34 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState } } val assertLoadingState: Unit get() = ... 34

Slide 35

Slide 35 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState } } 35

Slide 36

Slide 36 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList } } 36

Slide 37

Slide 37 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() } } 37

Slide 38

Slide 38 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { } } } 38

Slide 39

Slide 39 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") } } } 39

Slide 40

Slide 40 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState } } } 40

Slide 41

Slide 41 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() } } } 41

Slide 42

Slide 42 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState } } } 42

Slide 43

Slide 43 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() } } } 43

Slide 44

Slide 44 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState } } } 44

Slide 45

Slide 45 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } } } 45

Slide 46

Slide 46 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertContentState + itemToAdd } } 46

Slide 47

Slide 47 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled } } 47

Slide 48

Slide 48 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled } } } 48

Slide 49

Slide 49 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled clickFirstItem() } } } 49

Slide 50

Slide 50 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled clickFirstItem() assertContentStateWithFirstItemDone } } } 50

Slide 51

Slide 51 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled clickFirstItem() assertContentStateWithFirstItemDone clickFirstItem() } } } 51

Slide 52

Slide 52 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled clickFirstItem() assertContentStateWithFirstItemDone clickFirstItem() assertContentStateWithFirstItemNotDone } } } 52

Slide 53

Slide 53 text

@Test fun navigateBackAndForthInCreateItemWizard() { given { initialState = SummaryState("Some item") createItem { assertSummaryState pressBack() assertEnterTitleState pressNext() assertSummaryState } } } 53

Slide 54

Slide 54 text

but …. HOW? 54

Slide 55

Slide 55 text

State Based Architecture - Business Logic based on state machines - Single source of truth - State gets “rendered” on screen - Atomic UI updates - Push, not pull 55

Slide 56

Slide 56 text

Data Access Layer Application Layer / Business Logic Presentation Layer 56

Slide 57

Slide 57 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer 57

Slide 58

Slide 58 text

interface TodoRepository { fun getAll() : Observable> fun add(item : TodoItem) fun update(item : TodoItem) } 58

Slide 59

Slide 59 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer 59

Slide 60

Slide 60 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine 60

Slide 61

Slide 61 text

class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { val state: Observable fun input(action: Action) { } } 61

Slide 62

Slide 62 text

class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { sealed class State { object Loading : State() data class Content(val items: List) : State() object Error : State() } val state: Observable fun input(action: Action) { } } 62

Slide 63

Slide 63 text

class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { sealed class State { object Loading : State() data class Content(val items: List) : State() object Error : State() } val state: Observable = repository.getAll() .map { State.Content(it) as State } .onErrorReturn { State.Error } .startWith(State.Loading) fun input(action: Action) { } } 63

Slide 64

Slide 64 text

class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { sealed class State { object Loading : State() data class Content(val items: List) : State() object Error : State() } val state: Observable = ... fun input(action: Action) { } } 64

Slide 65

Slide 65 text

class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { sealed class State { object Loading : State() data class Content(val items: List) : State() object Error : State() } sealed class Action { data class ToggleTodoItemDoneAction(val item: TodoItem) : Action() data class DeleteTodoItemAction(val item: TodoItem) : Action() } val state: Observable = ... fun input(action: Action) { } } 65

Slide 66

Slide 66 text

class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { sealed class State { object Loading : State() data class Content(val items: List) : State() object Error : State() } sealed class Action { data class ToggleTodoItemDoneAction(val item: TodoItem) : Action() data class DeleteTodoItemAction(val item: TodoItem) : Action() } val state: Observable = ... fun input(action: Action) { when (action) { ... } } } 66

Slide 67

Slide 67 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine 67

Slide 68

Slide 68 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel 68

Slide 69

Slide 69 text

class TodoListViewModel @Inject constructor( private val stateMachine: TodoListStateMachine ) : ViewModel() { val state = MutableLiveData() fun input(action: Action) { } } 69

Slide 70

Slide 70 text

class TodoListViewModel @Inject constructor( private val stateMachine: TodoListStateMachine ) : ViewModel() { private val disposable: Disposable = stateMachine.state .subscribeOn(Schedulers.io()) .subscribe { state.postValue(it) }, val state = MutableLiveData() fun input(action: Action) { stateMachine.input(action) } override fun onCleared() { disposable.dispose() } } 70

Slide 71

Slide 71 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel pure functions 71

Slide 72

Slide 72 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment pure functions 72

Slide 73

Slide 73 text

class TodoListViewFragment : Fragment() { @Inject lateinit var viewModel : TodoListViewModel @Inject lateinit var viewBinder: TodoListViewBinder override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, b: Bundle?): View? = inflater.inflate(R.layout.fragment_todolist, c, false) override fun onStart() { super.onStart() viewModel.state.observe(this, Observer { viewBinder.render(it) }) viewBinder.actionListener = viewModel::input } } 73

Slide 74

Slide 74 text

class TodoListViewFragment : Fragment() { @Inject lateinit var viewModel : TodoListViewModel @Inject lateinit var viewBinder: TodoListViewBinder override fun onCreateView(inflater: LayoutInflater, c: ViewGroup?, b: Bundle?): View? = inflater.inflate(R.layout.fragment_todolist, container, false) override fun onStart() { super.onStart() viewModel.state.observe(this, Observer { viewBinder.render(it) }) viewBinder.actionListener = viewModel::input } } 74

Slide 75

Slide 75 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment 75

Slide 76

Slide 76 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder 76

Slide 77

Slide 77 text

open class TodoListViewBinder(private val root: View) { lateinit var actionListener: (Action) -> Unit private val adapter = TodoListAdapter({ actionListener(it) }) private val recyclerView = root.findViewById(R.id.recyclerView) private val error = root.findViewById(R.id.error) private val loading = root.findViewById(R.id.loading) open fun render(state: State) = when (state) { TodoListStateMachine.State.Loading -> { loading.visible() ... } TodoListStateMachine.State.Error -> { error.visible() ... } is TodoListStateMachine.State.Content -> { recyclerView.visible() adapter.items = state.items adapter.notifyDataSetChanged() ... } } } 77

Slide 78

Slide 78 text

open class TodoListViewBinder(private val root: View) { lateinit var actionListener: (Action) -> Unit private val adapter = TodoListAdapter({ actionListener(it) }) private val recyclerView = root.findViewById(R.id.recyclerView) private val error = root.findViewById(R.id.error) private val loading = root.findViewById(R.id.loading) open fun render(state: State) = when (state) { TodoListStateMachine.State.Loading -> { loading.visible() ... } TodoListStateMachine.State.Error -> { error.visible() ... } is TodoListStateMachine.State.Content -> { recyclerView.visible() adapter.items = state.items adapter.notifyDataSetChanged() ... } } } 78

Slide 79

Slide 79 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder 79

Slide 80

Slide 80 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder 80

Slide 81

Slide 81 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder TodoListRobot 81

Slide 82

Slide 82 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions TodoListRobot 82

Slide 83

Slide 83 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions ? TodoListRobot 83

Slide 84

Slide 84 text

84

Slide 85

Slide 85 text

@Test fun test1() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } 85

Slide 86

Slide 86 text

@Test fun test1() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test2() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } 86

Slide 87

Slide 87 text

@Test fun test1() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test2() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test3() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test4() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test5() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test6() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll() ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } @Test fun test7() { val repository = Mockito.mock `when`( repository.getAll() ) verify( repository, times(1) } @Test fun test8() { val repository = Mockito.moc `when`( repository.getAll() verify( repository, times(1) } @Test fun test9() { val repository = Mockito.m `when`( repository.getAll( verify( repository, times( } 87

Slide 88

Slide 88 text

88

Slide 89

Slide 89 text

89

Slide 90

Slide 90 text

write fakes / test implementations instead Refactoring Reusable 90

Slide 91

Slide 91 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions ? TodoListRobot 91

Slide 92

Slide 92 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot 92

Slide 93

Slide 93 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot Config 93

Slide 94

Slide 94 text

@Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled ... todoList { ... } } } 94

Slide 95

Slide 95 text

fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule) configBlock(config) } 95

Slide 96

Slide 96 text

fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule) configBlock(config) } class Config { var prefilledTodoItems = emptyList() } 96

Slide 97

Slide 97 text

fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule) configBlock(config) } class Config { var prefilledTodoItems = emptyList() fun todoList(block: TodoListRobot.() -> Unit) { } } 97

Slide 98

Slide 98 text

fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule) configBlock(config) } class Config { var prefilledTodoItems = emptyList() fun todoList(block: TodoListRobot.() -> Unit) { val todoRepository : InMemoryTodoRepository = ... } } 98

Slide 99

Slide 99 text

fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule) configBlock(config) } class Config { var prefilledTodoItems = emptyList() fun todoList(block: TodoListRobot.() -> Unit) { val todoRepository : InMemoryTodoRepository = ... todoRepository.clear() prefilledTodoItems.forEach { todoRepository.add(it) } } } 99

Slide 100

Slide 100 text

fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule) configBlock(config) } class Config { var prefilledTodoItems = emptyList() fun todoList(block: TodoListRobot.() -> Unit) { val todoRepository : InMemoryTodoRepository = ... todoRepository.clear() prefilledTodoItems.forEach { todoRepository.add(it) } val robot = TodoListRobot() block(robot) } } 100

Slide 101

Slide 101 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot Config 101

Slide 102

Slide 102 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertContentState + itemToAdd } } 102

Slide 103

Slide 103 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertContentState + itemToAdd } } 103

Slide 104

Slide 104 text

class TodoListRobot { } 104

Slide 105

Slide 105 text

class TodoListRobot { fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 105

Slide 106

Slide 106 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot Config 106

Slide 107

Slide 107 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository ListenableTodoListViewBinder TodoListRobot Config 107

Slide 108

Slide 108 text

class ListenableTodoListViewBinder(root: View) : TodoListViewBinder(root) { lateinit var stateChangedListener: (State) -> Unit override fun render(state: TodoListStateMachine.State) { super.render(state) stateChangedListener(state) } } 108

Slide 109

Slide 109 text

class TodoListRobot { fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 109

Slide 110

Slide 110 text

class TodoListRobot { init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> ... } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 110

Slide 111

Slide 111 text

class TodoListRobot { private val stateHistory = List() init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.add(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 111

Slide 112

Slide 112 text

class TodoListRobot { private val stateHistory = List() init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.add(state) } } fun assertStates(vararg states: State){ } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 112

Slide 113

Slide 113 text

class TodoListRobot { private val stateHistory = List() init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.add(state) } } fun assertStates(vararg states: State){ Assert.equals(stateHistory, states.toList()) } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 113

Slide 114

Slide 114 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertContentState + itemToAdd } } 114

Slide 115

Slide 115 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertStates(State.Loading) assertStates(State.Loading, State.Content(emptyList()) clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertStates(State.Loading, State.Content(emptyList()), State.Content(itemToAdd)) } } 115

Slide 116

Slide 116 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertStates(State.Loading) assertStates(State.Loading, State.Content(emptyList()) clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertStates(State.Loading, State.Content(emptyList()), State.Content(itemToAdd)) } } 116

Slide 117

Slide 117 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertStates(State.Loading) assertStates(State.Loading, State.Content(emptyList()) clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertStates(State.Loading, State.Content(emptyList()), State.Content(itemToAdd)) } } 117

Slide 118

Slide 118 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "A", false) given { prefilledTodoItems = emptyList() todoList { assertStates(State.Loading) assertStates(State.Loading, State.Content(emptyList()) clickCreateTodoItem() createItem { enterTitle("A") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertStates(State.Loading, State.Content(emptyList()), State.Content(itemToAdd)) } } 118

Slide 119

Slide 119 text

class TodoListRobot { private val stateHistory = List() init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.add(state) } } fun assertStates(vararg states: State){ Assert.equals(stateHistory, states.toList()) } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 119

Slide 120

Slide 120 text

class TodoListRobot { private val stateHistory = List() init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.add(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 120

Slide 121

Slide 121 text

class TodoListRobot { private val stateHistory = ReplayRelay.create() init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.accept(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 121

Slide 122

Slide 122 text

class TodoListRobot { private val stateHistory = ReplayRelay.create() private val stateVerifier = StateVerifier(stateHistory) init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.accept(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 122

Slide 123

Slide 123 text

private class StateVerifier(private val stateObservable: ReplayRelay) { } 123

Slide 124

Slide 124 text

private class StateVerifier(private val stateObservable: ReplayRelay) { @Synchronized fun assertNextState(nextExpectedState: S) { } } 124

Slide 125

Slide 125 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState } } 125

Slide 126

Slide 126 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable } } 126

Slide 127

Slide 127 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable .take(alreadyVerifiedStates.size + 1) } } 127

Slide 128

Slide 128 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable .take(alreadyVerifiedStates.size + 1) .timeout(10, TimeUnit.SECONDS) } } 128

Slide 129

Slide 129 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable .take(alreadyVerifiedStates.size + 1) .timeout(10, TimeUnit.SECONDS) .toList() } } 129

Slide 130

Slide 130 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable .take(alreadyVerifiedStates.size + 1) .timeout(10, TimeUnit.SECONDS) .toList() .blockingGet() } } 130

Slide 131

Slide 131 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable .take(alreadyVerifiedStates.size + 1) .timeout(10, TimeUnit.SECONDS) .toList() .blockingGet() Assert.assertEquals(expectedStates, actualStates) } } 131

Slide 132

Slide 132 text

private class StateVerifier(private val stateObservable: ReplayRelay) { private var alreadyVerifiedStates: List = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List = alreadyVerifiedStates + nextExpectedState val actualStates : List = stateObservable .take(alreadyVerifiedStates.size + 1) .timeout(10, TimeUnit.SECONDS) .toList() .blockingGet() Assert.assertEquals(expectedStates, actualStates) alreadyVerifiedStates = actualStates } } 132

Slide 133

Slide 133 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository ListenableTodoListViewBinder TodoListRobot Config 133

Slide 134

Slide 134 text

Data Access Layer TodoRepository Application Layer / Business Logic Presentation Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository ListenableTodoListViewBinder TodoListRobot StateVerifier Config 134

Slide 135

Slide 135 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "A", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("A") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertContentState + itemToAdd } } 135

Slide 136

Slide 136 text

class TodoListRobot { private val stateHistory = ReplayRelay.create() private val stateVerifier = StateVerifier(stateHistory) init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.accept(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 136

Slide 137

Slide 137 text

class TodoListRobot { private val stateHistory = ReplayRelay.create() private val stateVerifier = StateVerifier(stateHistory) init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.accept(state) } } val assertLoadingState: Unit get() = stateVerifier.assertNextState(TodoListStateMachine.State.Loading) fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 137

Slide 138

Slide 138 text

class TodoListRobot { private val stateHistory = ReplayRelay.create() private val stateVerifier = StateVerifier(stateHistory) init { val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.accept(state) } } val assertLoadingState: Unit get() = stateVerifier.assertNextState(TodoListStateMachine.State.Loading) val assertContentStateWithEmptyList: Unit get() = stateVerifier.assertNextState(TodoListStateMachine.State.Content(emptyList())) fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 138

Slide 139

Slide 139 text

@Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item", false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() assertSummaryState pressSave() assertSavingInProgressState assertSavingSuccessfulState } assertContentState + itemToAdd } } 139

Slide 140

Slide 140 text

Observation No UI verification by using Espresso? 140

Slide 141

Slide 141 text

class ListenableTodoListViewBinder(private val root: View) : TodoListViewBinder(root) { lateinit var stateChangedListener: (State) -> Unit override fun render(state: TodoListStateMachine.State) { super.render(state) stateChangedListener(state) } } 141

Slide 142

Slide 142 text

class ListenableTodoListViewBinder(private val root: View) : TodoListViewBinder(root) { lateinit var stateChangedListener: (State) -> Unit override fun render(state: TodoListStateMachine.State) { super.render(state) stateChangedListener(state) Screenshot.snap(root).record() } } http://facebook.github.io/screenshot-tests-for-android/ 142

Slide 143

Slide 143 text

Localization QA - Freeletics App is localized (9 languages) - Are all translations correct? - Use same Robots just without assertions - Navigate through our App - Use custom ViewBinder to create screenshots 143

Slide 144

Slide 144 text

Q & A 144

Slide 145

Slide 145 text

Summary - Robot Pattern is very readable - Adding configuration and assertions → reads like specifications - Test as much production code as possible - State based Architecture helps - Robots could be used without real UI (jvm tests) 145