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.

Malvin Sutanto

April 06, 2021
Tweet

More Decks by Malvin Sutanto

Other Decks in Programming

Transcript

  1. ©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
  2. ©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO

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

    J04.BJO BOESPJE.BJO Automated test Bottleneck
  4. ©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
  5. ©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")) } } } }
  6. ©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) }
  7. ©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) }
  8. ©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") } } } }
  9. ©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") } } } }
  10. ©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") } } } }
  11. ©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() } } }
  12. ©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() } } }
  13. ©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() } } }
  14. ©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() } } }
  15. ©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() } }
  16. ©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() } }
  17. ©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 } } }
  18. ©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 } } }
  19. ©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 } } }
  20. ©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 } } }
  21. ©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 } } }
  22. ©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 } } }
  23. ©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 } } }
  24. ©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 } }
  25. ©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 } }
  26. ©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 } }
  27. ©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
  28. ©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
  29. ©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
  30. ©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
  31. ©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
  32. ©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
  33. ©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 } }
  34. ©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 } }
  35. ©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 } }
  36. ©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 } }
  37. ©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))
  38. ©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) ...
  39. ©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) } } }
  40. ©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
  41. ©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
  42. ©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) } } }
  43. ©2021 Wantedly, Inc. Turbine https://github.com/cashapp/turbine A small testing library to

    test kotlinx.coroutines Flow 3rd Party Libraries for Testing Turbine
  44. ©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()
  45. ©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
  46. ©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