Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing By Design

Testing By Design

Building an efficient and maintainable test suite for an app is challenging. Design patterns like MVP or MVVM help decouple responsibilities so that it becomes easier to write testable code. What is the next level though? How can we get more out of our tests? How can we achieve the same results while writing less code for testing? How can we make tests resilient to minor changes? What if we take all this into account from the very beginning instead of making testing an afterthought? What if our architecture is built for testing by design?

In this talk we are going to discuss how a testing-first architecture such as MVI helps us to write and maintain efficient tests.

Hannes Dorfmann

May 22, 2019
Tweet

More Decks by Hannes Dorfmann

Other Decks in Technology

Transcript

  1. 3

  2. 9

  3. 10

  4. Challenges - Isolation of side effects - Unreliable slow layers:

    network, disk access, db access - Every code change affects tests 12
  5. 14

  6. 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
  7. Robot pattern. Key principles - Let robot do what user

    can do - Verify what user would see 19
  8. 20

  9. Robot pattern. Advantages - No need to change tests if

    the flow doesn’t change - Fun to read - Easy to extend 27
  10. Problems with “traditional” Robot Pattern - In which state is

    the Robot starting? - Assertions hidden - Doesn’t read like a specification 28
  11. @Test fun newTodoItemIsShownInTodoList() { val itemToAdd = TodoItem("1", "Another Item",

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

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

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

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

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

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

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

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

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

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

    false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() } } } 41
  22. @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
  23. @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
  24. @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
  25. @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
  26. @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
  27. @Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First

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

    Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled } } } 48
  29. @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
  30. @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
  31. @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
  32. @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
  33. @Test fun navigateBackAndForthInCreateItemWizard() { given { initialState = SummaryState("Some item")

    createItem { assertSummaryState pressBack() assertEnterTitleState pressNext() assertSummaryState } } } 53
  34. 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
  35. class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { sealed class

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

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

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

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

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

    ViewModel() { val state = MutableLiveData<State>() fun input(action: Action) { } } 69
  41. 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<State>() fun input(action: Action) { stateMachine.input(action) } override fun onCleared() { disposable.dispose() } } 70
  42. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment pure functions 72
  44. 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
  45. 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
  46. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder 76
  48. open class TodoListViewBinder(private val root: View) { lateinit var actionListener:

    (Action) -> Unit private val adapter = TodoListAdapter({ actionListener(it) }) private val recyclerView = root.findViewById<RecyclerView>(R.id.recyclerView) private val error = root.findViewById<View>(R.id.error) private val loading = root.findViewById<View>(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
  49. open class TodoListViewBinder(private val root: View) { lateinit var actionListener:

    (Action) -> Unit private val adapter = TodoListAdapter({ actionListener(it) }) private val recyclerView = root.findViewById<RecyclerView>(R.id.recyclerView) private val error = root.findViewById<View>(R.id.error) private val loading = root.findViewById<View>(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
  50. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

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

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

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions ? TodoListRobot 83
  55. 84

  56. @Test fun test1() { val repository = Mockito.mock(TodoRepository::class.java) `when`( repository.getAll()

    ).thenReturn( ... ) verify( repository, times(1) ).addItem(someItem) } 85
  57. @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
  58. @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
  59. 88

  60. 89

  61. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot Config 93
  64. @Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First

    Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled ... todoList { ... } } } 94
  65. fun given(configBlock: Config.() -> Unit) { val config = Config(activityRule)

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

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

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

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

    configBlock(config) } class Config { var prefilledTodoItems = emptyList<TodoItem>() fun todoList(block: TodoListRobot.() -> Unit) { val todoRepository : InMemoryTodoRepository = ... todoRepository.clear() prefilledTodoItems.forEach { todoRepository.add(it) } val robot = TodoListRobot() block(robot) } } 100
  70. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot Config 101
  71. @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
  72. @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
  73. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository ListenableTodoListViewBinder TodoListRobot Config 107
  75. class ListenableTodoListViewBinder(root: View) : TodoListViewBinder(root) { lateinit var stateChangedListener: (State)

    -> Unit override fun render(state: TodoListStateMachine.State) { super.render(state) stateChangedListener(state) } } 108
  76. class TodoListRobot { init { val viewBinder : ListenableTodoListViewBinder =

    ... viewBinder.stateChangedListener = { state -> ... } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 110
  77. class TodoListRobot { private val stateHistory = List<State>() init {

    val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.add(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 111
  78. class TodoListRobot { private val stateHistory = List<State>() 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
  79. class TodoListRobot { private val stateHistory = List<State>() 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
  80. @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
  81. @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
  82. @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
  83. @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
  84. @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
  85. class TodoListRobot { private val stateHistory = List<State>() 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
  86. class TodoListRobot { private val stateHistory = List<State>() init {

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

    val viewBinder : ListenableTodoListViewBinder = ... viewBinder.stateChangedListener = { state -> stateHistory.accept(state) } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 121
  88. class TodoListRobot { private val stateHistory = ReplayRelay.create<State>() 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
  89. private class StateVerifier<S>(private val stateObservable: ReplayRelay<S>) { private var alreadyVerifiedStates:

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

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

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

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

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

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

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

    List<S> = emptyList() @Synchronized fun assertNextState(nextExpectedState: S) { val expectedStates : List<S> = alreadyVerifiedStates + nextExpectedState val actualStates : List<S> = stateObservable .take(alreadyVerifiedStates.size + 1) .timeout(10, TimeUnit.SECONDS) .toList() .blockingGet() Assert.assertEquals(expectedStates, actualStates) alreadyVerifiedStates = actualStates } } 132
  97. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository ListenableTodoListViewBinder TodoListRobot StateVerifier Config 134
  99. @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
  100. class TodoListRobot { private val stateHistory = ReplayRelay.create<State>() 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
  101. class TodoListRobot { private val stateHistory = ReplayRelay.create<State>() 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
  102. class TodoListRobot { private val stateHistory = ReplayRelay.create<State>() 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
  103. @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
  104. 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
  105. 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
  106. 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
  107. 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