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

Testing for Android developers

Testing for Android developers

Getting started with testing in Android in 2023 is easier than ever before. We will be covering some fundamentals of thoughts around testing as well as getting into actual examples of how to test your code.

Avatar for Peter Törnhult

Peter Törnhult

April 18, 2023

More Decks by Peter Törnhult

Other Decks in Technology

Transcript

  1. Agenda • Welcome to B3 (safety guidelines and brief company

    overview) • Pragmatic testing in Android by Peter • Jetpack Compose Animations by Erik • Snacks and mingle
  2. Unit testing in Android • JUnit 3, 4 or 5

    (other?) • Assertion libraries: ◦ org.junit.* ◦ Google Truth ◦ Kluent ◦ Hamcrest ◦ Gurken ◦ Etc… • Given, when, then • @VisibleForTesting • What NOT to test • Mock, stub, spy (Mockito or MockK) • Android context ◦ Mock ◦ Move to integration tests ◦ Robolectric
  3. assertEquals(2, 1 + 1) assertNotEquals(1, 2) assertTrue(true) assertFalse(false) assertNull(null) assertNotNull(1)

    assertSame(1, 1) assertNotSame(128, 128) ... assertX(expected = …, actual = …)
  4. @Test(expected = ArrayIndexOutOfBoundsException:: class) fun `test one exception old style`

    () { emptyArray<Int>()[ 0] } @Test fun `test multiple exceptions new style` () { assertThrows(ArrayIndexOutOfBoundsException:: class.java) { emptyArray<Int>()[ 0] } }
  5. class Example { fun one() = 1 } class ExampleTest

    { private val example = Example() @Test fun `one should return 1` () { assertEquals( 1, Example().one()) } }
  6. class Example { fun isCat(animal: Animal): Boolean { return if

    (animal is Cat) true else false } @Test fun `isCat should return true for Cats` () { assertTrue( example.isCat(Cat( "Whiskers"))) assertFalse( example.isCat(Dog( "Dogzilla")))}
  7. class Example { fun isOne(input: Int): Boolean = input ==

    1 } @Test fun `isOne should return true for input 1` () { assertTrue( example.isOne(1)) assertFalse( example.isOne(-1)) assertFalse( example.isOne(0)) assertFalse( example.isOne(2)) }
  8. @Before data class Example(var state: Any? = null) private val

    example = Example() @Test fun `state 1`() { assertNull( example.state) example.state = 1 assertEquals( 1, example.state) } @Test fun `state 2`() { assertNull( example.state) // Works in Unit tests example.state = 2 assertEquals( 2, example.state) }
  9. @Before data class Example(var state: Any? = null) companion object

    { private val example = Example() } @Test fun `state 1`() { assertNull( example.state) example.state = 1 assertEquals( 1, example.state) } @Test fun `state 2`() { assertNull( example.state) // Fails example.state = 2 assertEquals( 2, example.state) }
  10. @Before data class Example(var state: Any? = null) companion object

    { private lateinit var example: Example } @Before fun setUp() { example = Example() assertNull( example.state) } @Test fun `state 1`() { example.state = 1 assertEquals( 1, example.state) } @Test fun `state 2`() { example.state = 2 assertEquals( 2, example.state) }
  11. @Before data class Example(var state: Any? = null) private lateinit

    var example: Example @Before fun setUp() { example = Example() }
  12. @Before data class Example(var state: Any? = null) private lateinit

    var example: Example @Before fun setUp() { example = Example() } @Test fun `state can be changed` () { assertNull( example.state) example.state = 1_000L assertEquals( 1_000L, example.state) }
  13. @BeforeClass, @AfterClass companion object { private lateinit var example: Example

    @BeforeClass @JvmStatic fun setUpClass() { example = Example() } }
  14. data class Example(var state: Any? = null) @Test fun `state

    can be changed` () { // Given val example = Example() // When example.state = 1_000L // Then assertEquals( 1_000L, example.state) }
  15. data class Example(var state: Any? = null) // Given private

    val example = Example() @Test fun `state can be changed` () { // When example.state = 1_000L // Then assertEquals( 1_000L, example.state) }
  16. What NOT to test / do • Dead code •

    Generated code • External libraries • No if-statements in tests • No sleep/pause/delay in tests • Hardware dependencies* class ExampleUnitTest { @Test fun addition_isCorrect () { assertEquals( 4, 2 + 2) } }
  17. Stub A stub is an object that provides (canned) hardcoded

    values to method calls. It always returns the same output regardless of the input… Mock A mock is similar to a stub, but the behaviour of the mocked interface can be changed dynamically based on scenarios… Fake A Fake is an object with a concrete implementation that works similarly to the actual implementation… Spy A spy lets us verify what functions were called, with what arguments, when, and how often… Test double, Dummy…
  18. Fake / Test double interface ExampleRepo { fun one(): Int

    } class Example: ExampleRepo { override fun one() = 1 } class ExampleTest { private lateinit var example: Example private lateinit var exampleRepo : ExampleRepo @Before fun setUp() { example = Example() exampleRepo = object: ExampleRepo { override fun one(): Int = 1 } } }
  19. Spy class Example{ fun isOne(input: Int): Boolean = input ==

    1 fun isNotOne(input: Int): Boolean = !isOne(input) @Test fun `isNotOne calls isOne once` () { val example = spyk(Example())
  20. Spy class Example{ fun isOne(input: Int): Boolean = input ==

    1 fun isNotOne(input: Int): Boolean = !isOne(input) @Test fun `isNotOne calls isOne once` () { val example = spyk(Example()) example.isNotOne(2) verify { example.isOne(any()) }
  21. Spy @Test fun `isNotOne calls isOne once` () { val

    example = spyk(Example()) example.isNotOne(2) verify { example.isOne(any()) } verify(exactly = 1) { example.isOne(2) } verify(exactly = 0) { example.isOne(1) } class Example{ fun isOne(input: Int): Boolean = input == 1 fun isNotOne(input: Int): Boolean = !isOne(input)
  22. Mock @Test fun `two returns 2 and calls one only

    once` () { val example = spyk(Example()) val metaExample = MetaExample(example) assertEquals( 2, metaExample.two()) verify(exactly = 1) { example.one() } } class MetaExample( private val exampleRepo : ExampleRepo , ) { fun two() = exampleRepo .one() * 2 }
  23. Mock @Test fun `two returns 2 and calls one only

    once` () { val example: Example = mockk() val metaExample = MetaExample(example) assertEquals( 2, metaExample.two()) verify(exactly = 1) { example.one() } } class MetaExample( private val exampleRepo : ExampleRepo , ) { fun two() = exampleRepo .one() * 2 }
  24. Mock @Test fun `two returns 2 and calls one only

    once` () { val example: Example = mockk { every { one() } returns 1 } val metaExample = MetaExample(example) assertEquals( 2, metaExample.two()) verify(exactly = 1) { example.one() } } class MetaExample( private val exampleRepo : ExampleRepo , ) { fun two() = exampleRepo .one() * 2 }
  25. Mock @Test fun `two calls one only once` () {

    val example: Example = mockk { every { one() } returns 1 } val metaExample = MetaExample(example) verify(exactly = 1) { example.one() } } class MetaExample( private val exampleRepo : ExampleRepo , ) { fun two() = exampleRepo .one() * 2 }
  26. Mock @Test fun `two calls one only once` () {

    val example: Example = mockk(relaxed = true) val metaExample = MetaExample(example) verify(exactly = 1) { example.one() } } class MetaExample( private val exampleRepo : ExampleRepo , ) { fun two() = exampleRepo .one() * 2 }
  27. MockK can also mock… https://mockk.io/ • Coroutines • Top level

    / Static functions • Extension functions • Objects • Private functions • … And supports • Varargs • Chained calls • unmocking • …
  28. Flaky tests Start small - Verify app can start -

    Verify home page is shown - Verify navigating to next screen works
  29. Flaky tests Start small Remove all I/O from tests -

    Database (Room) -> In memory / fake
  30. Flaky tests Start small Remove all I/O from tests -

    Database (Room) -> In memory / fake - Networking (OkHttp + Retrofit) -> Mock server + canned responses
  31. Flaky tests Start small Remove all I/O from tests -

    Database (Room) -> In memory / fake - Networking (OkHttp + Retrofit) -> Mock server + canned responses - Local storage -> Mock repos or clear before each test
  32. Flaky tests Start small Remove all I/O from tests -

    Database (Room) -> In memory / fake - Networking (OkHttp + Retrofit) -> Mock server + canned responses - Local storage -> Mock repos or clear before each test - Local files -> Mock repos or clear before each tests
  33. Flaky tests Start small Remove all I/O from tests -

    Database (Room) -> In memory / fake - Networking (OkHttp + Retrofit) -> Mock server + canned responses - Local storage -> Mock repos or clear before each test - Local files -> Mock repos or clear before each tests - Other -> Mock
  34. Hilt ❤ Integration Tests @Module @TestInstallIn ( components = [SingletonComponent::

    class], replaces = [AppDatabaseModule:: class] ) object FakeAppDatabaseModule { @Singleton @Provides fun providesAppDatabase (@ApplicationContext context: Context): AppDatabase = Room.inMemoryDatabaseBuilder(context. applicationContext, AppDatabase:: class.java) .setTransactionExecutor(Executors.newSingleThreadExecutor()) .build()
  35. Tests during migration to Compose Espresso and ComposeTestRule can work

    together but… Maybe just comment everything out 🤯 …at first
  36. Compose testing Semantics tree - Tag - Text - Content

    description Icon(contentDescription = "Icon", …) composeTestRule .onNodeWithContentDescription("Icon") *Only supports strings
  37. Compose testing Semantics tree - Tag - Text - Content

    description - Missing performLongClick(...) Experimental, easy to add
  38. Compose testing Semantics tree - Tag - Text - Content

    description - Missing performLongClick(...) - Missing pressBack() Use Espresso.pressBack()
  39. Compose testing Semantics tree - Tag - Text - Content

    description - Missing performLongClick(...) - Missing pressBack() - Easy to use and extend
  40. Compose testing Semantics tree - Tag - Text - Content

    description - Missing performLongClick(...) - Missing pressBack() - Easy to use and extend See Kakao
  41. How and where to run Integration tests Local emulator(s) Local

    test device(s) Google Test Lab (or similar) Managed devices