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.
    Automated Testing
    in a KMM Project
    KMM勉強会
    Apr 6, 2021 - Malvin Sutanto

    View Slide

  2. ©2021 Wantedly, Inc.
    Introduction

    View Slide

  3. ©2021 Wantedly, Inc.
    Introduction
    Malvin Sutanto
    Software Engineer - Android
    Twitter/ Medium: @malvinsutanto

    View Slide

  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

    View Slide

  5. ©2021 Wantedly, Inc.
    Testing in a KMM project

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. ©2021 Wantedly, Inc.
    Writing Tests

    View Slide

  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

    View Slide

  14. ©2021 Wantedly, Inc.
    Sample Implementation
    https://github.com/touchlab/KaMPKit
    Page Title Page Subtitle

    View Slide

  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"))
    }
    }
    }
    }

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  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")
    }
    }
    }
    }

    View Slide

  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")
    }
    }
    }
    }

    View Slide

  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")
    }
    }
    }
    }

    View Slide

  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() }
    }
    }

    View Slide

  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() }
    }
    }

    View Slide

  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() }
    }
    }

    View Slide

  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() }
    }
    }

    View Slide

  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()
    }
    }

    View Slide

  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()
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  37. ©2021 Wantedly, Inc.
    Running Tests

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  42. ©2021 Wantedly, Inc.
    3rd Party Libraries for Testing

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)
    ...

    View Slide

  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)
    }
    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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)
    }
    }
    }

    View Slide

  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

    View Slide

  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()

    View Slide

  57. ©2021 Wantedly, Inc.
    Issues with Testing in a KMM Project

    View Slide

  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

    View Slide

  59. ©2021 Wantedly, Inc.
    Summary

    View Slide

  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

    View Slide

  61. ©2021 Wantedly, Inc.
    Thank You!
    @MalvinSutanto
    Page Title Page Subtitle

    View Slide