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

Instrumentation Testing Strategies

Instrumentation Testing Strategies

Tips for maximizing value from Android tests

- Core principles of testing
- Write once, run everywhere: shared tests with Robolectric + AndroidX Test
- Robot pattern for UI tests

https://www.meetup.com/Android-Meetup/events/264205264/

Linked resources in slides:

- Frictionless Android testing: write once, run everywhere (Google I/O '18)
https://www.youtube.com/watch?v=wYMIadv9iF8
- Testing Android Apps at Scale with Nitrogen (Android Dev Summit '18)
https://www.youtube.com/watch?v=-_kZC29sWAo
- Instrumentation Testing Robots by Jake Wharton
https://academy.realm.io/posts/kau-jake-wharton-testing-robots/
- Demo application
https://github.com/ssaqua/WallPad

Sung-Soo Hong

September 10, 2019
Tweet

Other Decks in Programming

Transcript

  1. Core principles Write once, run everywhere - shared tests with

    Robolectric + AndroidX Test Robot pattern - manageable UI tests
  2. Why do we write tests? Proves your code is working

    as intended Ensures your code keeps working as intended Form of documentation Enables confidence to refactor Release frequently
  3. Tests are a long term commitment Treat tests as production

    code Fix failing tests or bugs in tests Refactor existing tests Remove obsolete tests Tests require infrastructure CI servers Influences application architecture
  4. Unit Tests Run on local JVM No dependencies on Android

    framework (or mocked) Execute quickly, on the order of milliseconds Cannot accept any flakiness Focused on small units, optimized for failure
  5. Instrumentation Tests Run on Android environment Higher fidelity at the

    expense of execution time and debugging Difficult to avoid flakiness Possible to be agnostic to app architecture "The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License."
  6. class RepositoryTest { private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() lateinit var repository: Repository @Before fun setUp() { repository = TestHelper.createRepository(mockService, mockCache) }} @Test fun `getItems() requests from service when cache is empty`() { val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  7. class RepositoryTest { private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() lateinit var repository: Repository @Before fun setUp() { repository = TestHelper.createRepository(mockService, mockCache) }} @Test fun `getItems() requests from service when cache is empty`() { val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  8. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() lateinit var repository: Repository @Before fun setUp() { repository = TestHelper.createRepository(mockService, mockCache) }} @Test fun `getItems() requests from service when cache is empty`() {{ val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  9. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = TestHelper.createRepository(mockService, mockCache) val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  10. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = TestHelper.createRepository(mockService, mockCache) val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  11. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) val_serviceItems = listOf(Item()) `when`(mockService.getItems()).thenReturn(serviceItems) val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  12. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) val_serviceItems = listOf(Item()) `when`(mockService.getItems()).thenReturn(serviceItems) val items = repository.getItems() verify(mockService).getItems() checkItemsAreFromService(items) }} }}
  13. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) val_serviceItems = listOf(Item()) `when`(mockService.getItems()).thenReturn(serviceItems) val items = repository.getItems() verify(mockService).getItems() assertEquals(items, serviceItems) }} }}
  14. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) val_serviceItems = listOf(Item()) `when`(mockService.getItems()).thenReturn(serviceItems) val items = repository.getItems() verify(mockService).getItems() assertEquals(items, serviceItems) }} }}
  15. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) repository.getItems() verify(mockService).getItems() }} }}
  16. class RepositoryTest {{ private val mockService: Service = mock() private

    val mockCache: Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) repository.getItems() verify(mockService).getItems() }} }}
  17. class RepositoryTest {{ val mockService: Service = mock() val mockCache:

    Cache<List<Item>> = mock() @Test fun `getItems() requests from service when cache is empty`() {{ val repository = Repository(mockService,_mockCache)) `when`(mockCache.get()).thenReturn(emptyList()) repository.getItems() verify(mockService).getItems() }} }}
  18. Prioritize readability for tests Violating DRY or including magic numbers

    is acceptable Minimize any layers of abstraction OOP encapsulation rarely matters for testing code Consider disabling MemberVisibilityCanBePrivate inspection in tests scope
  19. Dependencies testImplementation 'androidx.test:runner:1.2.0' testImplementation 'androidx.test:core-ktx:1.2.0' testImplementation 'androidx.test.ext:junit:1.1.1' testImplementation 'androidx.test.espresso:espresso-core:3.2.0' testImplementation

    'org.robolectric:robolectric:4.3' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:core-ktx:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
  20. Dependencies testImplementation 'androidx.test:runner:1.2.0' testImplementation 'androidx.test:core-ktx:1.2.0' testImplementation 'androidx.test.ext:junit:1.1.1' testImplementation 'androidx.test.espresso:espresso-core:3.2.0' testImplementation

    'org.robolectric:robolectric:4.3' testImplementation_'org.mockito:mockito-core:3.0.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:core-ktx:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation_'org.mockito:mockito-android:3.0.0' // or dexmaker
  21. Module build.gradle android { sourceSets { def sharedTestDir = 'src/sharedTest/java'

    test.java.srcDirs += sharedTestDir androidTest.java.srcDirs += sharedTestDir } testOptions { unitTests.includeAndroidResources = true } }
  22. @RunWith(AndroidJUnit4::class) class GalleryFragmentTest {{ @Test fun progressBarVisible_onLoadingStatus() {{ launchFragmentInContainer {{

    //_setup GalleryFragment with bundle args and mock VM here //_mock VM exposes imagesLiveData GalleryFragment() }} imagesLiveData.postValue(Resource(Status.LOADING)) onView(withId(R.id.progress_bar)).check(matches(isDisplayed())) onView(withId(R.id.empty_text)).check(matches(not(isDisplayed()))) onView(withId(R.id.error_text)).check(matches(not(isDisplayed()))) onView(withId(R.id.recycler_view)).check(matches(not(isDisplayed()))) }} }}
  23. @RunWith(AndroidJUnit4::class) // androidx.test.ext.junit.runners class GalleryFragmentTest {{ @Test fun progressBarVisible_onLoadingStatus() {{

    launchFragmentInContainer {{ //_setup GalleryFragment with bundle args and mock VM here //_mock VM exposes imagesLiveData GalleryFragment() }} imagesLiveData.postValue(Resource(Status.LOADING)) onView(withId(R.id.progress_bar)).check(matches(isDisplayed())) onView(withId(R.id.empty_text)).check(matches(not(isDisplayed()))) onView(withId(R.id.error_text)).check(matches(not(isDisplayed()))) onView(withId(R.id.recycler_view)).check(matches(not(isDisplayed()))) }{ }}
  24. Project Nitrogen Single entry point for Android tests Execute in

    any environment Virtual device management Covers the entire test lifecycle: configuration, execution, reporting Testing Android Apps at Scale with Nitrogen (Android Dev Summit '18) Frictionless Android testing: write once, run everywhere (Google I/O '18)
  25. class PhotoViewerRobot { fun save() { ... } fun hasPhotoWithId(id:

    String) { ... } fun navToSearch() { ... } }
  26. class PhotoViewerRobot { fun save() { ... } fun hasPhotoWithId(id:

    String) { ... } fun navToSearch() { ... } fun navToSaved() { ... } }
  27. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} fun navToSaved() {} } @Test fun saveImage() { }
  28. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} fun navToSaved() {} } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { }
  29. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} fun navToSaved() {} } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { photoViewer { } }
  30. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} fun navToSaved() {} } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { photoViewer { hasPhotoWithId("test_photo") save() } }
  31. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} fun navToSaved(block): SavedRobot {} } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { photoViewer { hasPhotoWithId("test_photo") save() } }
  32. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} fun navToSaved(block): SavedRobot {} } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { photoViewer { hasPhotoWithId("test_photo") save() }.navToSaved { hasPhotoWithId("test_photo") } }
  33. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} infix fun navToSaved(block) ... } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { photoViewer { hasPhotoWithId("test_photo") save() }.navToSaved { hasPhotoWithId("test_photo") } }
  34. class PhotoViewerRobot { // actions and assertions fun save() {}

    fun hasPhotoWithId(id: String) {} fun navToSearch() {} infix fun navToSaved(block) ... } fun photoViewer( block: PhotoViewerRobot.() -> Unit ): PhotoViewerRobot { return PhotoViewerRobot().apply(block) } @Test fun saveImage() { photoViewer { hasPhotoWithId("test_photo") save() } navToSaved { hasPhotoWithId("test_photo") } }
  35. class PhotoViewerRobot( block: PhotoViewerRobot.() -> Unit ) { init {

    block() } } @Test fun saveImage() { PhotoViewerRobot { hasPhotoWithId("test_photo") save() navToSaved() } SavedRobot { hasPhotoWithId("test_photo") } }