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

Android Testing Strategy

Android Testing Strategy

Testing an Android application was always hard. In the past couple of years we got access to better tools, but we still rely a lot on architecture and design decisions in order to properly test an Android application. This talk will discuss the layers of automated testing an Android application should have, from End-to-end, Component, Integration to Unit tests. Also we will discuss BDD, TDD and Clean Architecture as tools to implement those layers.

Marcelo Benites

November 07, 2018
Tweet

More Decks by Marcelo Benites

Other Decks in Programming

Transcript

  1. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) }
  2. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls) ) }
  3. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) }
  4. data class State<T>(val name: Name, val value: T? = null)

    { enum class Name { IDLE, LOADING, LOADED, ERROR } }
  5. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() }
  6. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) }
  7. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) callManager.loadCalls() }
  8. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) callManager.loadCalls() val callsCaptor = argumentCaptor<State<List<Call>>>() verify(listenerMock, times(3)).onCallUpdate(callsCaptor.capture()) }
  9. @Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),

    Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) callManager.loadCalls() val callsCaptor = argumentCaptor<State<List<Call>>>() verify(listenerMock, times(3)).onCallUpdate(callsCaptor.capture()) assertEquals(State(State.Name.LOADED, calls), callsCaptor.lastValue) }
  10. class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,

    private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) { }
  11. class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,

    private val publishScheduler: Scheduler, private var callsState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { }
  12. class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,

    private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) } private fun updateCurrentState(calls: State<List<Call>>) { this.currentState = calls this.listener?.onCallUpdate(currentState) } }
  13. class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,

    private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) Single .fromCallable { callGateway.getCalls() } .subscribeOn(scheduler) .observeOn(publishScheduler) } private fun updateCurrentState(calls: State<List<Call>>) { this.currentState = calls this.listener?.onCallUpdate(currentState) } }
  14. class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,

    private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) Single .fromCallable { callGateway.getCalls() } .subscribeOn(scheduler) .observeOn(publishScheduler) .subscribe ( { calls -> updateCurrentState(currentState.copy(name = State.Name.LOADED, value = calls))}, { updateCurrentState(currentState.copy(name = State.Name.ERROR, value = null))} ) } private fun updateCurrentState(calls: State<List<Call>>) { this.currentState = calls this.listener?.onCallUpdate(currentState) } }
  15. class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,

    private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) Single .fromCallable { callGateway.getCalls() } .subscribeOn(scheduler) .observeOn(publishScheduler) .subscribe ( { calls -> updateCurrentState(currentState.copy(name = State.Name.LOADED, value = calls))}, { updateCurrentState(currentState.copy(name = State.Name.ERROR, value = null))} ) } override fun registerListener(listener: CallListener) { this.listener = listener this.listener?.onCallUpdate(currentState) } private fun updateCallsState(calls: State<List<Call>>) { this.callsState = calls this.listener?.onCallUpdate(callsState) } }
  16. @Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val

    baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val baseUrl = server.url("/").toString() val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) }
  17. @Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val

    baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) gateway.getCalls() }
  18. @Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val

    baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) assertEquals(calls, gateway.getCalls()) }
  19. @Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val

    baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) assertEquals(calls, gateway.getCalls()) val request = server.takeRequest() assertEquals( "GET", request.method) assertEquals("/Calls", request.path) }
  20. @Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val

    baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) assertEquals(calls, gateway.getCalls()) val request = server.takeRequest() assertEquals( "GET", request.method) assertEquals("/Calls", request.path) server.shutdown() }
  21. class FakeCallManager( private val calls: State<List<Call>> ) : CallManager {

    private var listener: CallListener? = null override fun loadCalls() { listener?.onCallUpdate(calls) } override fun registerListener(listener: CallListener) { this.listener = listener this.listener?.onCallUpdate(calls) } }
  22. class CallActivity: AppCompatActivity(), CallFragmentContainer { private lateinit var dependencyManager: DependencyManager

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_call) dependencyManager = application as DependencyManager } override fun getCallManager() = dependencyManager.getCallManager() }
  23. class CallFragment : Fragment() { private lateinit var container: CallFragmentContainer

    override fun onAttach(context: Context?) { super.onAttach(context) if (context is CallFragmentContainer) { container = context } else { throw IllegalStateException("Context must implement CallFragmentContainer") } } ….. }
  24. class CallFragment : Fragment() { private lateinit var container: CallFragmentContainer

    private lateinit var callManager: CallManager override fun onAttach(context: Context?) { super.onAttach(context) if (context is CallFragmentContainer) { container = context } else { throw IllegalStateException("Context must implement CallFragmentContainer") } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) callManager = container.getCallManager() } ….. }
  25. @Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @Test fun shouldShowLoadedCalls()

    { val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) } }
  26. @Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @Test fun shouldShowLoadedCalls()

    { val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) } }
  27. @Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @get:Rule val rule:

    ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java, true, false) @Test fun shouldShowLoadedCalls() { val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) rule.launchActivity(Intent()) } }
  28. @Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @get:Rule val rule:

    ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java, true, false) @Test fun shouldShowLoadedCalls() { val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) rule.launchActivity(Intent()) onView(withText("John Lennon")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Ringo Starr")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("George Harrison")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Paul Mccartney")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } }
  29. class HttpCallGatewayIntegrationTest { @Test fun shouldReturnCallsFromHttpServer() { val calls =

    listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway("http://www.dev.talkdesk.com/", JsonObjectConverter(), OkHttpClient()) } }
  30. class HttpCallGatewayIntegrationTest { @Test fun shouldReturnCallsFromHttpServer() { val calls =

    listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway("http://www.dev.talkdesk.com/", JsonObjectConverter(), OkHttpClient()) gateway.getCalls() } }
  31. class HttpCallGatewayIntegrationTest { @Test fun shouldReturnCallsFromHttpServer() { val calls =

    listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway("http://www.dev.talkdesk.com/", JsonObjectConverter(), OkHttpClient()) assertEquals(calls, gateway.getCalls()) } }
  32. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { val server

    = MockWebServer() server.start() val json = """ [ { "contact": "John Lennon" }, { "contact": "Ringo Starr" }, { "contact": "George Harrison" }, { "contact": "Paul Mccartney" } ] """.trimIndent() server.enqueue(MockResponse().setResponseCode(200).setBody(json)) } }
  33. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer

    setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) } }
  34. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer

    setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) } }
  35. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer

    setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) } }
  36. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer

    setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) } }
  37. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer

    setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) } }
  38. @RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer

    setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) val callApplication = ApplicationProvider.getApplicationContext<CallApplication>() val field = callApplication.javaClass.getDeclaredField("callManager") field.isAccessible = true field.set(callApplication, callManager) } }
  39. @RunWith(AndroidJUnit4::class) class CallComponentTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,

    true, false) @Test fun shouldShowCalls() { // Arrange - omitted for brevity callManager.loadCalls() rule.launchActivity(Intent()) } }
  40. @RunWith(AndroidJUnit4::class) class CallComponentTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,

    true, false) @Test fun shouldShowCalls() { // Arrange - omitted for brevity callManager.loadCalls() rule.launchActivity(Intent()) onView(withText("John Lennon")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Ringo Starr")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("George Harrison")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Paul Mccartney")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) server.shutdown() } }
  41. @RunWith(AndroidJUnit4::class) class CallEndToEndTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,

    true, false) @Test fun shouldShowCalls() { rule.launchActivity(Intent()) Thread.sleep(2000) onView(withText("John Lennon")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Ringo Starr")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("George Harrison")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Paul Mccartney")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) } }