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. Testing By Design 1

  2. Kostia Tarasenko Hannes Dorfmann 2

  3. 3

  4. How to write maintainable tests and how proper architecture can

    help you 4
  5. Unit tests White box testing of individual software components in

    isolation 5
  6. Integration tests “Narrow” definition: Testing the code that connects different

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

    output 7
  8. End-to-end tests Functional tests backed by real environment 8

  9. 9

  10. 10

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

    11
  12. Challenges - Isolation of side effects - Unreliable slow layers:

    network, disk access, db access - Every code change affects tests 12
  13. What if? … writing and maintaining functional tests was easier?

    13
  14. 14

  15. 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
  16. @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
  17. Write tests yo mama would be proud of 17

  18. Robot pattern - influenced by Martin Fowler’s PageObject pattern -

    Embraced by kotlin DSL syntax 18
  19. Robot pattern. Key principles - Let robot do what user

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

  21. @Test fun newTodoItemIsShownInTodoList() { } 21

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

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

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

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

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

    Item") pressNext() pressSave() } } } 26
  27. Robot pattern. Advantages - No need to change tests if

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    false) given { prefilledTodoItems = emptyList() todoList { assertLoadingState assertContentStateWithEmptyList clickCreateTodoItem() createItem { enterTitle("Another Item") assertEnterTitleState pressNext() } } } 41
  42. @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
  43. @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
  44. @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
  45. @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
  46. @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
  47. @Test fun markAsDoneAndNotDone() { val prefilled = listOf( TodoItem("1", "First

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

    Item", false), TodoItem("2", "Second item", true) ) given { prefilledTodoItems = prefilled todoList { assertLoadingState assertContentState + prefilled } } } 48
  49. @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
  50. @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
  51. @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
  52. @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
  53. @Test fun navigateBackAndForthInCreateItemWizard() { given { initialState = SummaryState("Some item")

    createItem { assertSummaryState pressBack() assertEnterTitleState pressNext() assertSummaryState } } } 53
  54. but …. HOW? 54

  55. 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
  56. Data Access Layer Application Layer / Business Logic Presentation Layer

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

    Layer 57
  58. interface TodoRepository { fun getAll() : Observable<List<TodoItem>> fun add(item :

    TodoItem) fun update(item : TodoItem) } 58
  59. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine 60
  61. class TodoListStateMachine @Inject constructor(private val repository: TodoRepository) { val state:

    Observable<State> fun input(action: Action) { } } 61
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine TodoListViewModel 68
  69. class TodoListViewModel @Inject constructor( private val stateMachine: TodoListStateMachine ) :

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

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

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

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder 76
  77. 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
  78. 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
  79. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

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

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

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

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

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

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

  89. 89

  90. write fakes / test implementations instead Refactoring Reusable 90

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

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

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

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

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

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

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

    configBlock(config) } class Config { var prefilledTodoItems = emptyList<TodoItem>() fun todoList(block: TodoListRobot.() -> Unit) { } } 97
  98. 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
  99. 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
  100. 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
  101. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository TodoListRobot Config 101
  102. @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
  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 } } 103
  104. class TodoListRobot { } 104

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

    105
  106. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

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

    -> Unit override fun render(state: TodoListStateMachine.State) { super.render(state) stateChangedListener(state) } } 108
  109. class TodoListRobot { fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } }

    109
  110. class TodoListRobot { init { val viewBinder : ListenableTodoListViewBinder =

    ... viewBinder.stateChangedListener = { state -> ... } } fun clickCreateTodoItem() { Espresso.onView(ViewMatchers.withId(R.id.newItem)) .perform(ViewActions.click()) } } 110
  111. 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
  112. 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
  113. 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
  114. @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
  115. @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
  116. @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
  117. @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
  118. @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
  119. 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
  120. 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
  121. 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
  122. 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
  123. private class StateVerifier<S>(private val stateObservable: ReplayRelay<S>) { } 123

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

    S) { } } 124
  125. 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
  126. 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
  127. 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
  128. 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
  129. 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
  130. 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
  131. 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
  132. 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
  133. Data Access Layer TodoRepository Application Layer / Business Logic Presentation

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

    Layer TodoListStateMachine TodoListViewModel TodoListFragment TodoListViewBinder pure functions InMemoryTodoRepository ListenableTodoListViewBinder TodoListRobot StateVerifier Config 134
  135. @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
  136. 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
  137. 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
  138. 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
  139. @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
  140. Observation No UI verification by using Espresso? 140

  141. 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
  142. 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
  143. 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
  144. Q & A 144

  145. 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