Why and how to write unit tests in a KMM project.
©2021 Wantedly, Inc.Automated Testingin a KMM ProjectKMM勉強会Apr 6, 2021 - Malvin Sutanto
View Slide
©2021 Wantedly, Inc.Introduction
©2021 Wantedly, Inc.IntroductionMalvin SutantoSoftware Engineer - AndroidTwitter/ Medium: @malvinsutanto
©2021 Wantedly, Inc.1. Testing in a KMM project2. Writing Tests3. Running KMM Tests4. 3rd Party Libraries for Writing Tests5. Issues with Testing in a KMM ProjectAgenda
©2021 Wantedly, Inc.Testing in a KMM project
©2021 Wantedly, Inc.KMM Structure6*'SBNFXPSL6*""3DPNNPO.BJOJ04.BJO BOESPJE.BJO
©2021 Wantedly, Inc.KMM Structure6*'SBNFXPSL6*""3DPNNPO.BJOJ04.BJO BOESPJE.BJOManual test
©2021 Wantedly, Inc.KMM Structure6*'SBNFXPSL6*""3DPNNPO.BJOJ04.BJO BOESPJE.BJOManual testBottleneck
©2021 Wantedly, Inc.KMM Structure6*'SBNFXPSL6*""3DPNNPO.BJOJ04.BJO BOESPJE.BJOAutomated testBottleneck
©2021 Wantedly, Inc.KMM Structure6*'SBNFXPSL6*""3DPNNPO.BJOJ04.BJO BOESPJE.BJOFlakyBottleneck
©2021 Wantedly, Inc.KMM Structure6*'SBNFXPSL6*""3DPNNPO.BJOJ04.BJO BOESPJE.BJOAutomated test
©2021 Wantedly, Inc.Writing Tests
©2021 Wantedly, Inc.KMM Module Test Folder StructureDPNNPO5FTUJ045FTUBOESPJE5FTUTests that will be run on both iOS and Android platformTests that will be run only on iOS platformTests that will be run only on Android platform
©2021 Wantedly, Inc.Sample Implementationhttps://github.com/touchlab/KaMPKitPage Title Page Subtitle
©2021 Wantedly, Inc.KMM Module Test Folder StructureDPNNPO5FTU// kmm-module/build.gradle.ktskotlin {...sourceSets {...val commonTest by getting {dependencies {implementation(kotlin("test-common"))implementation(kotlin("test-annotations-common"))}}}}
©2021 Wantedly, Inc.KMM Module Test Folder StructureDPNNPO5FTU// kmm-module/src/commonTest/kotlin/BaseTest.ktimport kotlinx.coroutines.CoroutineScopeexpect abstract class BaseTest() {fun runTest(block: suspend CoroutineScope.() -> Unit)}
©2021 Wantedly, Inc.KMM Module Test Folder StructureBOESPJE5FTU// kmm-module/build.gradle.ktskotlin {...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")}}}}
©2021 Wantedly, Inc.KMM Module Test Folder StructureBOESPJE5FTU// kmm-module/src/androidTest/kotlin/BaseTest.kt@RunWith(AndroidJUnit4::class)@Config(sdk = [30])actual abstract class BaseTest {@get:Ruleval coroutineTestRule = CoroutineTestRule()actual fun runTest(block: suspend CoroutineScope.() -> Unit) {runBlocking { block() }}}
©2021 Wantedly, Inc.KMM Module Test Folder StructureBOESPJE5FTU// kmm-module/src/androidTest/kotlin/BaseTest.ktclass 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()}}
©2021 Wantedly, Inc.KMM Module Test Folder StructureJPT5FTU// kmm-module/src/iosTest/kotlin/BaseTest.ktactual abstract class BaseTest {actual fun runTest(block: suspend CoroutineScope.() -> Unit) {var error: Throwable? = nullGlobalScope.launch(Dispatchers.Main) {try {block()} catch (t: Throwable) {error = t} finally {CFRunLoopStop(CFRunLoopGetCurrent())}}CFRunLoopRun()error?.also { throw it }}}
©2021 Wantedly, Inc.KMM Module Test Folder StructureDPNNPO5FTU// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `my simple test`() {assertEquals(2, 1 + 1)}@Testfun `my coroutines test`() = runTest {assertEquals(2, mySum(1, 1))}private suspend fun mySum(a: Int, b: Int): Int {delay(200)return a + b}}
©2021 Wantedly, Inc.Running Tests
©2021 Wantedly, Inc.$ gradlew :kmm-module:test$ gradlew :kmm-module:testDebugUnitTest$ gradlew :kmm-module:testReleaseUnitTest$ gradlew :kmm-module:iosTestX64TestRunning TestsIntellij IDE Gradle Command
©2021 Wantedly, Inc.$ gradlew :kmm-module:test$ gradlew :kmm-module:testDebugUnitTest$ gradlew :kmm-module:testReleaseUnitTest$ gradlew :kmm-module:iosTestX64TestRunning TestsIntellij IDE Gradle CommandAndroid: commonTest + androidTest
©2021 Wantedly, Inc.$ gradlew :kmm-module:test$ gradlew :kmm-module:testDebugUnitTest$ gradlew :kmm-module:testReleaseUnitTest$ gradlew :kmm-module:iosTestX64TestRunning TestsIntellij IDE Gradle CommandiOS: commonTest + iOSTest
©2021 Wantedly, Inc.$ gradlew :kmm-module:test$ gradlew :kmm-module:testDebugUnitTest$ gradlew :kmm-module:testReleaseUnitTest$ gradlew :kmm-module:iosTestX64TestRunning TestsIntellij IDE Gradle CommandAll platforms: commonTest + iOSTest + androidTest
©2021 Wantedly, Inc.3rd Party Libraries for Testing
©2021 Wantedly, Inc.Kotesthttps://kotest.ioMultiplatform test framework, assertion libraries, and property test library for Kotlin.3rd Party Libraries for Testing Kotest4USVDUVSFZPVSUFTUXJUI#FIBWJPS4QFD4FUPGNBUDIFSTUPWBMJEBUFUFTUT7BMVFHFOFSBUPSTGPSUFTUJOHFEHFDBTFTBOESBOEPNWBMVFT
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - assertions library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `my simple test`() {assertEquals(2, 1 + 1)}@Testfun `my coroutines test`() = runTest {assertEquals(2, mySum(1, 1))}private suspend fun mySum(a: Int, b: Int): Int {delay(200)return a + b}}
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - assertions library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `my simple test`() {1 + 1 shouldBe 2}@Testfun `my coroutines test`() = runTest {assertEquals(2, mySum(1, 1))}private suspend fun mySum(a: Int, b: Int): Int {delay(200)return a + b}}
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - assertions library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `my simple test`() {1 + 1 shouldBe 2}@Testfun `my coroutines test`() = runTest {mySum(1, 1) shouldBe 2}private suspend fun mySum(a: Int, b: Int): Int {delay(200)return a + b}}
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - assertions library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `my simple test`() {1 + 1 shouldBe 2}@Testfun `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))
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - property test library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `my coroutines test`() = runTest {mySum(1, 1) shouldBe 2}}mySum(-2, -3)mySum(2, -3)mySum(-2, 3)mySum(2, 0)...
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - property test library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `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)}}}
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - property test library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `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
©2021 Wantedly, Inc.3rd Party Libraries for Testing Kotest - property test library// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun `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
©2021 Wantedly, Inc.Turbinehttps://github.com/cashapp/turbineA small testing library to test kotlinx.coroutines Flow3rd Party Libraries for Testing Turbine
©2021 Wantedly, Inc.3rd Party Libraries for Testing Turbine// kmm-module/src/commonTest/kotlin/MyTest.ktclass MyTest : BaseTest() {@Testfun myFlowTest() = runTest {flowOf("one", "two").test {expectItem() shouldBe "one"expectItem() shouldBe "two"expectComplete()}}}expectError()expectEvent()expectNoEvents()cancel()cancelAndIgnoreRemainingEvents()
©2021 Wantedly, Inc.Issues with Testing in a KMM Project
©2021 Wantedly, Inc.1. Test Doubles (Fakes, Mocks, etc)• MockK does not yet support Kotlin/Native2. Testing with Coroutines• kotlinx-coroutines-test only supports Kotlin/JVM for now3. Code coverage• Currently impossible to measure4. Integration and E2E tests• Creates Fake implementation whose behavior can be overriddenIssues with Testing in a KMM Project
©2021 Wantedly, Inc.Summary
©2021 Wantedly, Inc.1. Manual UI testing with a KMM project isdifficult2. Write automated test to ensure BusinessLogic is implemented correctly3. Use 3rd party libraries to help write unit testsSummary
©2021 Wantedly, Inc.Thank You!@MalvinSutantoPage Title Page Subtitle