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

Automated Testing in a KMM Project

Automated Testing in a KMM Project

Why and how to write unit tests in a KMM project.

6338c8fa4e2e6325094fe30b1e9f9443?s=128

Malvin Sutanto

April 06, 2021
Tweet

Transcript

  1. ©2021 Wantedly, Inc. Automated Testing in a KMM Project KMM勉強会

    Apr 6, 2021 - Malvin Sutanto
  2. ©2021 Wantedly, Inc. Introduction

  3. ©2021 Wantedly, Inc. Introduction Malvin Sutanto Software Engineer - Android

    Twitter/ Medium: @malvinsutanto
  4. ©2021 Wantedly, Inc. 1. Testing in a KMM project 2.

    Writing Tests 3. Running KMM Tests 4. 3rd Party Libraries for Writing Tests 5. Issues with Testing in a KMM Project Agenda
  5. ©2021 Wantedly, Inc. Testing in a KMM project

  6. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

    J04.BJO BOESPJE.BJO
  7. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

    J04.BJO BOESPJE.BJO Manual test
  8. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

    J04.BJO BOESPJE.BJO Manual test Bottleneck
  9. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

    J04.BJO BOESPJE.BJO Automated test Bottleneck
  10. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

    J04.BJO BOESPJE.BJO Flaky Bottleneck
  11. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

    J04.BJO BOESPJE.BJO Automated test
  12. ©2021 Wantedly, Inc. Writing Tests

  13. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU J045FTU

    BOESPJE5FTU Tests that will be run on both iOS and Android platform Tests that will be run only on iOS platform Tests that will be run only on Android platform
  14. ©2021 Wantedly, Inc. Sample Implementation https://github.com/touchlab/KaMPKit Page Title Page Subtitle

  15. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU //

    kmm-module/build.gradle.kts kotlin { ... sourceSets { ... val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } } }
  16. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU //

    kmm-module/src/commonTest/kotlin/BaseTest.kt import kotlinx.coroutines.CoroutineScope expect abstract class BaseTest() { fun runTest(block: suspend CoroutineScope.() -> Unit) }
  17. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU //

    kmm-module/src/commonTest/kotlin/BaseTest.kt import kotlinx.coroutines.CoroutineScope expect abstract class BaseTest() { fun runTest(block: suspend CoroutineScope.() -> Unit) }
  18. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/build.gradle.kts kotlin { ... sourceSets { ... val androidTest by getting { dependencies { implementation(kotlin("test-junit")) implementation("junit:junit:4.13") implementation("androidx.test:core:1.3.0") implementation("androidx.test:runner:1.3.0") implementation("androidx.test:rules:1.3.0") implementation("androidx.test.ext:junit-ktx:1.1.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3") implementation("org.robolectric:robolectric:4.4") } } } }
  19. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/build.gradle.kts kotlin { ... sourceSets { ... val androidTest by getting { dependencies { implementation(kotlin("test-junit")) implementation("junit:junit:4.13") implementation("androidx.test:core:1.3.0") implementation("androidx.test:runner:1.3.0") implementation("androidx.test:rules:1.3.0") implementation("androidx.test.ext:junit-ktx:1.1.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3") implementation("org.robolectric:robolectric:4.4") } } } }
  20. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/build.gradle.kts kotlin { ... sourceSets { ... val androidTest by getting { dependencies { implementation(kotlin("test-junit")) implementation("junit:junit:4.13") implementation("androidx.test:core:1.3.0") implementation("androidx.test:runner:1.3.0") implementation("androidx.test:rules:1.3.0") implementation("androidx.test.ext:junit-ktx:1.1.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3") implementation("org.robolectric:robolectric:4.4") } } } }
  21. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/src/androidTest/kotlin/BaseTest.kt @RunWith(AndroidJUnit4::class) @Config(sdk = [30]) actual abstract class BaseTest { @get:Rule val coroutineTestRule = CoroutineTestRule() actual fun runTest(block: suspend CoroutineScope.() -> Unit) { runBlocking { block() } } }
  22. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/src/androidTest/kotlin/BaseTest.kt @RunWith(AndroidJUnit4::class) @Config(sdk = [30]) actual abstract class BaseTest { @get:Rule val coroutineTestRule = CoroutineTestRule() actual fun runTest(block: suspend CoroutineScope.() -> Unit) { runBlocking { block() } } }
  23. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/src/androidTest/kotlin/BaseTest.kt @RunWith(AndroidJUnit4::class) @Config(sdk = [30]) actual abstract class BaseTest { @get:Rule val coroutineTestRule = CoroutineTestRule() actual fun runTest(block: suspend CoroutineScope.() -> Unit) { runBlocking { block() } } }
  24. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/src/androidTest/kotlin/BaseTest.kt @RunWith(AndroidJUnit4::class) @Config(sdk = [30]) actual abstract class BaseTest { @get:Rule val coroutineTestRule = CoroutineTestRule() actual fun runTest(block: suspend CoroutineScope.() -> Unit) { runBlocking { block() } } }
  25. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/src/androidTest/kotlin/BaseTest.kt class CoroutineTestRule( private val testDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor() .asCoroutineDispatcher() ) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() } }
  26. ©2021 Wantedly, Inc. KMM Module Test Folder Structure BOESPJE5FTU //

    kmm-module/src/androidTest/kotlin/BaseTest.kt class CoroutineTestRule( private val testDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor() .asCoroutineDispatcher() ) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() } }
  27. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  28. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  29. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  30. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  31. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  32. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  33. ©2021 Wantedly, Inc. KMM Module Test Folder Structure JPT5FTU //

    kmm-module/src/iosTest/kotlin/BaseTest.kt actual abstract class BaseTest { actual fun runTest(block: suspend CoroutineScope.() -> Unit) { var error: Throwable? = null GlobalScope.launch(Dispatchers.Main) { try { block() } catch (t: Throwable) { error = t } finally { CFRunLoopStop(CFRunLoopGetCurrent()) } } CFRunLoopRun() error?.also { throw it } } }
  34. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU //

    kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { assertEquals(2, 1 + 1) } @Test fun `my coroutines test`() = runTest { assertEquals(2, mySum(1, 1)) } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  35. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU //

    kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { assertEquals(2, 1 + 1) } @Test fun `my coroutines test`() = runTest { assertEquals(2, mySum(1, 1)) } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  36. ©2021 Wantedly, Inc. KMM Module Test Folder Structure DPNNPO5FTU //

    kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { assertEquals(2, 1 + 1) } @Test fun `my coroutines test`() = runTest { assertEquals(2, mySum(1, 1)) } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  37. ©2021 Wantedly, Inc. Running Tests

  38. ©2021 Wantedly, Inc. $ gradlew :kmm-module:test $ gradlew :kmm-module:testDebugUnitTest $

    gradlew :kmm-module:testReleaseUnitTest $ gradlew :kmm-module:iosTestX64Test Running Tests Intellij IDE Gradle Command
  39. ©2021 Wantedly, Inc. $ gradlew :kmm-module:test $ gradlew :kmm-module:testDebugUnitTest $

    gradlew :kmm-module:testReleaseUnitTest $ gradlew :kmm-module:iosTestX64Test Running Tests Intellij IDE Gradle Command Android: commonTest + androidTest
  40. ©2021 Wantedly, Inc. $ gradlew :kmm-module:test $ gradlew :kmm-module:testDebugUnitTest $

    gradlew :kmm-module:testReleaseUnitTest $ gradlew :kmm-module:iosTestX64Test Running Tests Intellij IDE Gradle Command iOS: commonTest + iOSTest
  41. ©2021 Wantedly, Inc. $ gradlew :kmm-module:test $ gradlew :kmm-module:testDebugUnitTest $

    gradlew :kmm-module:testReleaseUnitTest $ gradlew :kmm-module:iosTestX64Test Running Tests Intellij IDE Gradle Command All platforms: commonTest + iOSTest + androidTest
  42. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing

  43. ©2021 Wantedly, Inc. Kotest https://kotest.io Multiplatform test framework, assertion libraries,

    and property test library for Kotlin. 3rd Party Libraries for Testing Kotest 4USVDUVSFZPVSUFTUXJUI #FIBWJPS4QFD 4FUPGNBUDIFSTUP WBMJEBUFUFTUT 7BMVFHFOFSBUPSTGPS UFTUJOHFEHFDBTFTBOE SBOEPNWBMVFT
  44. ©2021 Wantedly, Inc. Kotest https://kotest.io Multiplatform test framework, assertion libraries,

    and property test library for Kotlin. 3rd Party Libraries for Testing Kotest 4USVDUVSFZPVSUFTUXJUI #FIBWJPS4QFD 4FUPGNBUDIFSTUP WBMJEBUFUFTUT 7BMVFHFOFSBUPSTGPS UFTUJOHFEHFDBTFTBOE SBOEPNWBMVFT
  45. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    assertions library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { assertEquals(2, 1 + 1) } @Test fun `my coroutines test`() = runTest { assertEquals(2, mySum(1, 1)) } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  46. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    assertions library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { 1 + 1 shouldBe 2 } @Test fun `my coroutines test`() = runTest { assertEquals(2, mySum(1, 1)) } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  47. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    assertions library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { 1 + 1 shouldBe 2 } @Test fun `my coroutines test`() = runTest { assertEquals(2, mySum(1, 1)) } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  48. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    assertions library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { 1 + 1 shouldBe 2 } @Test fun `my coroutines test`() = runTest { mySum(1, 1) shouldBe 2 } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } }
  49. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    assertions library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my simple test`() { 1 + 1 shouldBe 2 } @Test fun `my coroutines test`() = runTest { mySum(1, 1) shouldBe 2 } private suspend fun mySum(a: Int, b: Int): Int { delay(200) return a + b } } assertEquals(2, 1 + 1) assertEquals(2, mySum(1, 1))
  50. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    property test library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my coroutines test`() = runTest { mySum(1, 1) shouldBe 2 } } mySum(-2, -3) mySum(2, -3) mySum(-2, 3) mySum(2, 0) ...
  51. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    property test library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my coroutines test`() = runTest { checkAll( config = PropTestConfig(seed = 211125L), genA = Arb.int(-25..25), genB = Arb.int(-25..25), iterations = 100 ) { a, b -> mySum(a, b) shouldBe (a + b) } } }
  52. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    property test library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my coroutines test`() = runTest { checkAll( config = PropTestConfig(seed = 211125L), genA = Arb.int(-25..25), genB = Arb.int(-25..25), iterations = 100 ) { a, b -> mySum(a, b) shouldBe (a + b) } } } Generate random values from -25 to 25
  53. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    property test library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my coroutines test`() = runTest { checkAll( config = PropTestConfig(seed = 211125L), genA = Arb.int(-25..25), genB = Arb.int(-25..25), iterations = 100 ) { a, b -> mySum(a, b) shouldBe (a + b) } } } Repeat 100 times
  54. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Kotest -

    property test library // kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun `my coroutines test`() = runTest { checkAll( config = PropTestConfig(seed = 211125L), genA = Arb.int(-25..25), genB = Arb.int(-25..25), iterations = 100 ) { a, b -> mySum(a, b) shouldBe (a + b) } } }
  55. ©2021 Wantedly, Inc. Turbine https://github.com/cashapp/turbine A small testing library to

    test kotlinx.coroutines Flow 3rd Party Libraries for Testing Turbine
  56. ©2021 Wantedly, Inc. 3rd Party Libraries for Testing Turbine //

    kmm-module/src/commonTest/kotlin/MyTest.kt class MyTest : BaseTest() { @Test fun myFlowTest() = runTest { flowOf("one", "two").test { expectItem() shouldBe "one" expectItem() shouldBe "two" expectComplete() } } } expectError() expectEvent() expectNoEvents() cancel() cancelAndIgnoreRemainingEvents()
  57. ©2021 Wantedly, Inc. Issues with Testing in a KMM Project

  58. ©2021 Wantedly, Inc. 1. Test Doubles (Fakes, Mocks, etc) •

    MockK does not yet support Kotlin/Native 2. Testing with Coroutines • kotlinx-coroutines-test only supports Kotlin/JVM for now 3. Code coverage • Currently impossible to measure 4. Integration and E2E tests • Creates Fake implementation whose behavior can be overridden Issues with Testing in a KMM Project
  59. ©2021 Wantedly, Inc. Summary

  60. ©2021 Wantedly, Inc. 1. Manual UI testing with a KMM

    project is difficult 2. Write automated test to ensure Business Logic is implemented correctly 3. Use 3rd party libraries to help write unit tests Summary
  61. ©2021 Wantedly, Inc. Thank You! @MalvinSutanto Page Title Page Subtitle