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

Droidcon - Android Testing Strategy

Droidcon - 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

September 10, 2019
Tweet

More Decks by Marcelo Benites

Other Decks in Programming

Transcript

  1. It is hard to fake Android OS events during testing

    (Battery Level, Location, Sensors etc...).
  2. Domain • Business Logic • No implementation details (HTTP, SQLite,

    Bluetooth, etc...) • No external dependencies (including Android SDK)
  3. Data • Simple adapter to a data source: Http, File

    System, Bluetooth, WebSocket, Push etc... • Should not be complex. Move complexity to the Domain • No external dependencies (including Android SDK)
  4. Controller Presenter Use Case Implementation Callback <i> Use Case <i>

    Flow of control Domain Gateway <i> Gateway Implementation Data Presentation
  5. Testing Strategy • Unit Tests - Test Presentation, Domain and

    Data in isolation. • Integration Tests - Test Data and Presentation interaction with External Dependencies. • Component Tests - Test Presentation, Domain and Data interaction using Test Doubles (Mock, Fake, Spy, Stub) for External Dependencies. • End-to-End Tests - Test Presentation, Domain Data and External Dependencies interaction.
  6. BDD

  7. Given a conference is registered with name Droidcon When I

    open conference application Then I see Droidcon on the screen
  8. TDD

  9. data class State<T>( val name: Name, val value: T? =

    null ) { enum class Name { IDLE, LOADING, LOADED, ERROR } } Idle Loading Loaded Error
  10. @Test fun `Given a registered conference When start is called

    Then should emit loaded state with conference`() { val conference = Conference("1", "Droidcon") val stateMachine = ConferenceStateMachine( FakeDispatcher(), FakeConferenceGateway(conference) ) val listenerMock = mockk<(State<Conference, GatewayError>) -> Unit>() stateMachine.addStateChangedListener(listenerMock) stateMachine.start() verifyOrder { listenerMock.invoke(State(State.Name.LOADING)) listenerMock.invoke(State(State.Name.LOADED, conference)) } }
  11. Act

  12. @Test fun `Given a conference When start is called Then

    should emit loaded state with conference`() { val conference = Conference("1", "Droidcon") val stateMachine = ConferenceStateMachine( FakeDispatcher(), FakeConferenceGateway(conference) ) val listenerMock = mockk<(State<Conference, GatewayError>) -> Unit>() stateMachine.addStateChangedListener(listenerMock) stateMachine.start() verifyOrder { listenerMock.invoke(State(State.Name.LOADING)) listenerMock.invoke(State(State.Name.LOADED, conference)) } }
  13. @Test fun `Given a conference When start is called Then

    should emit loaded state with conference`() { val conference = Conference("1", "Droidcon") val stateMachine = ConferenceStateMachine( FakeDispatcher(), FakeConferenceGateway(conference) ) val listenerMock = mockk<(State<Conference, GatewayError>) -> Unit>() stateMachine.addStateChangedListener(listenerMock) stateMachine.start() verifyOrder { listenerMock.invoke(State(State.Name.LOADING)) listenerMock.invoke(State(State.Name.LOADED, conference)) } }
  14. class ConferenceStateMachine( private val conferenceGateway: ConferenceGateway, private val dispatcher: Dispatcher

    ) : StateMachine<Conference>() { override fun start() { loadConference() } private fun loadConference() { dispatcher.dispatch({ moveToLoading() moveToLoaded(conferenceGateway.getConference()) }, { moveToError(it) }) } }
  15. interface ConferenceView { fun hideLoading() fun showLoading() fun hideError() fun

    showError() fun showConferenceName(name: String) fun hideConferenceName() }
  16. @Test fun `Given a loaded conference When state is updated

    Then show conference name`() { val view = mockk<ConferenceView>() val presenter = ConferencePresenter(FakeDispatcher(), view) presenter.invoke(State(State.Name.LOADED, Conference("1", "Droidcon"))) verify { view.hideError() view.hideLoading() view.showConferenceName("Droidcon") } }
  17. Act

  18. @Test fun `Given a loaded conference When state is updated

    Then show conference name`() { val view = mockk<ConferenceView>() val presenter = ConferencePresenter(FakeDispatcher(), view) presenter.invoke(State(State.Name.LOADED, Conference("1", "Droidcon"))) verify { view.hideError() view.hideLoading() view.showConferenceName("Droidcon") } }
  19. @Test fun `Given a loaded conference When state is updated

    Then show conference name`() { val view = mockk<ConferenceView>(relaxed = true) val presenter = ConferencePresenter(FakeDispatcher(), view) presenter.invoke(State(State.Name.LOADED, Conference("1", "Droidcon"))) verify { view.hideError() view.hideLoading() view.showConferenceName("Droidcon") } }
  20. @Test fun `When conference is requested Then call conference endpoint

    with GET method`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val gateway = OkHttpConferenceGateway(baseUrl, OkHttpClient()) val json = """ { "id": "1", "name": "Droidcon" } """ server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val conference = gateway.getConference() assertEquals(Conference("1", "Droidcon"), conference) val request = server.takeRequest() assertEquals("GET", request.method) assertEquals("/conference", request.path) server.shutdown() }
  21. Act

  22. @Test fun `When conference is requested Then call conference endpoint

    with GET method`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val gateway = OkHttpConferenceGateway(baseUrl, OkHttpClient()) val json = """ { "id": "1", "name": "Droidcon" } """.trimIndent() server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val conference = gateway.getConference() assertEquals(Conference("1", "Droidcon"), conference) val request = server.takeRequest() assertEquals("GET", request.method) assertEquals("/conference", request.path) server.shutdown() }
  23. @Test fun `When conference is requested Then call conference endpoint

    with GET method`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val gateway = OkHttpConferenceGateway(baseUrl, OkHttpClient()) val json = """ { "id": "1", "name": "Droidcon" } """.trimIndent() server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val conference = gateway.getConference() assertEquals(Conference("1", "Droidcon"), conference) val request = server.takeRequest() assertEquals("GET", request.method) assertEquals("/conference", request.path) server.shutdown() }
  24. class TestActivity : AppCompatActivity(), ViewContainer { var testDependencyManager: DependencyManager? =

    null override val dependencyManager: DependencyManager by lazy { testDependencyManager!! } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val view = LinearLayout(this) view.id = 1 setContentView(view) } fun showFragment(fragment: Fragment) { supportFragmentManager .beginTransaction() .replace(1, fragment) .commit() } }
  25. class TestActivity : AppCompatActivity(), ViewContainer { var testDependencyManager: DependencyManager? =

    null override val dependencyManager: DependencyManager by lazy { testDependencyManager!! } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val view = LinearLayout(this) view.id = 1 setContentView(view) } fun showFragment(fragment: Fragment) { supportFragmentManager .beginTransaction() .replace(1, fragment) .commit() } }
  26. @RunWith(AndroidJUnit4::class) class ConferenceComponentTest { @get:Rule val rule: ActivityTestRule<TestActivity> = ActivityTestRule(TestActivity::class.java)

    @Test fun `Given a registered conference When conference screen appears Then show conference name`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val json = """ { "id": "1", "name": "Droidcon" } """ server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val stateMachine = ConferenceStateMachine(OkHttpConferenceGateway(baseUrl, OkHttpClient()), FakeDispatcher()) stateMachine.start() rule.activity.testDependencyManager = FakeDependencyManager(stateMachine, FakeDispatcher()) rule.activity.showFragment(ConferenceFragment()) onView(withId(R.id.conferenceName)).check(matches(ViewMatchers.withText("Droidcon"))) server.shutdown() } }
  27. @RunWith(AndroidJUnit4::class) class ConferenceComponentTest { @get:Rule val rule: ActivityTestRule<TestActivity> = ActivityTestRule(TestActivity::class.java)

    @Test fun `Given a registered conference When conference screen appears Then show conference name`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val json = """ { "id": "1", "name": "Droidcon" } """ server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val stateMachine = ConferenceStateMachine(OkHttpConferenceGateway(baseUrl, OkHttpClient()), FakeDispatcher()) stateMachine.start() rule.activity.testDependencyManager = FakeDependencyManager(stateMachine, FakeDispatcher()) rule.activity.showFragment(ConferenceFragment()) onView(withId(R.id.conferenceName)).check(matches(ViewMatchers.withText("Droidcon"))) server.shutdown() } }
  28. Act

  29. @RunWith(AndroidJUnit4::class) class ConferenceComponentTest { @get:Rule val rule: ActivityTestRule<TestActivity> = ActivityTestRule(TestActivity::class.java)

    @Test fun `Given a registered conference When conference screen appears Then show conference name`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val json = """ { "id": "1", "name": "Droidcon" } """ server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val stateMachine = ConferenceStateMachine(OkHttpConferenceGateway(baseUrl, OkHttpClient()), FakeDispatcher()) stateMachine.start() rule.activity.testDependencyManager = FakeDependencyManager(stateMachine, FakeDispatcher()) rule.activity.showFragment(ConferenceFragment()) onView(withId(R.id.conferenceName)).check(matches(ViewMatchers.withText("Droidcon"))) server.shutdown() } }
  30. @RunWith(AndroidJUnit4::class) class ConferenceComponentTest { @get:Rule val rule: ActivityTestRule<TestActivity> = ActivityTestRule(TestActivity::class.java)

    @Test fun `Given a registered conference When conference screen appears Then show conference name`() { val server = MockWebServer() server.start() val baseUrl = server.url("/").toString() val json = """ { "id": "1", "name": "Droidcon" } """ server.enqueue(MockResponse().setResponseCode(200).setBody(json)) val stateMachine = ConferenceStateMachine(OkHttpConferenceGateway(baseUrl, OkHttpClient()), FakeDispatcher()) stateMachine.start() rule.activity.testDependencyManager = FakeDependencyManager(stateMachine, FakeDispatcher()) rule.activity.showFragment(ConferenceFragment()) onView(withId(R.id.conferenceName)).check(matches(ViewMatchers.withText("Droidcon"))) server.shutdown() } }
  31. Act

  32. @RunWith(AndroidJUnit4::class) class ConferenceEndToEndTest { @get:Rule val rule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java,

    true, false) @Test fun givenRegisteredConferenceWhenApplicationStartsStartShowConferenceName() { rule.launchActivity(Intent()) Thread.sleep(2000) onView(ViewMatchers.withId(R.id.conferenceName)).check(matches(withText("Droidcon"))) } }
  33. @RunWith(AndroidJUnit4::class) class ConferenceEndToEndTest { @get:Rule val rule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java,

    true, false) @Test fun givenRegisteredConferenceWhenApplicationStartsStartShowConferenceName() { rule.launchActivity(Intent()) Thread.sleep(2000) onView(ViewMatchers.withId(R.id.conferenceName)).check(matches(withText("Droidcon"))) } }
  34. Unit Tests Integration Tests Component Tests End-to-End Tests Domain Data

    Fake DB Fake External Service Presentation Fake GUI DB External Service Android SDK
  35. Reference • The Clean Architecture - Robert Martin (http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) •

    Test Double - Martin Fowler (https://martinfowler.com/bliki/TestDouble.html) • Testing Strategies in a Microservices Architecture - Martin Fowler (https://martinfowler.com/articles/microservice-testing/#conclusion-summary) • Managing State with RxJava - Jake Wharton (https://www.youtube.com/watch?v=0IKHxjkgop4) • Android Testing Strategy - Marcelo Benites (https://engineering.talkdesk.com/android-testing-strategy-73269539c13d)