Slide 1

Slide 1 text

©2021 Wantedly, Inc. Automated Testing in a KMM Project KMM勉強会 Apr 6, 2021 - Malvin Sutanto

Slide 2

Slide 2 text

©2021 Wantedly, Inc. Introduction

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

©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

Slide 5

Slide 5 text

©2021 Wantedly, Inc. Testing in a KMM project

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

©2021 Wantedly, Inc. Writing Tests

Slide 13

Slide 13 text

©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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

©2021 Wantedly, Inc. Running Tests

Slide 38

Slide 38 text

©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

Slide 39

Slide 39 text

©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

Slide 40

Slide 40 text

©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

Slide 41

Slide 41 text

©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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

©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

Slide 44

Slide 44 text

©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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

©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

Slide 53

Slide 53 text

©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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

©2021 Wantedly, Inc. Turbine https://github.com/cashapp/turbine A small testing library to test kotlinx.coroutines Flow 3rd Party Libraries for Testing Turbine

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

©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

Slide 59

Slide 59 text

©2021 Wantedly, Inc. Summary

Slide 60

Slide 60 text

©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

Slide 61

Slide 61 text

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