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

Transcript

114 slides
  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))) } }

More Decks by Marcelo Benites

Other Decks in Programming