Share
Embed
Automated Testing in a KMM Project
Why and how to write unit tests in a KMM project.
Malvin Sutanto
April 06, 2021
Transcript
61 slides
-
©2021 Wantedly, Inc. Automated Testing in a KMM Project KMM勉強会
Apr 6, 2021 - Malvin Sutanto -
©2021 Wantedly, Inc. Introduction Malvin Sutanto Software Engineer - Android
Twitter/ Medium: @malvinsutanto -
©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 -
©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO
J04.BJO BOESPJE.BJO -
©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO
J04.BJO BOESPJE.BJO Manual test -
©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO
J04.BJO BOESPJE.BJO Manual test Bottleneck -
©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO
J04.BJO BOESPJE.BJO Automated test Bottleneck -
©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO
J04.BJO BOESPJE.BJO Flaky Bottleneck -
©2021 Wantedly, Inc. KMM Structure 6* 'SBNFXPSL 6* ""3 DPNNPO.BJO
J04.BJO BOESPJE.BJO Automated test -
©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 -
©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")) } } } } -
©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) } -
©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) } -
©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") } } } } -
©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") } } } } -
©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") } } } } -
©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() } } } -
©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() } } } -
©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() } } } -
©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() } } } -
©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() } } -
©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() } } -
©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 } } } -
©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 } } } -
©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 } } } -
©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 } } } -
©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 } } } -
©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 } } } -
©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 } } } -
©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 } } -
©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 } } -
©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 } } -
©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 -
©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 -
©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 -
©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 -
©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 -
©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 -
©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 } } -
©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 } } -
©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 } } -
©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 } } -
©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)) -
©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) ... -
©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) } } } -
©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 -
©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 -
©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) } } } -
©2021 Wantedly, Inc. Turbine https://github.com/cashapp/turbine A small testing library to
test kotlinx.coroutines Flow 3rd Party Libraries for Testing Turbine -
©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() -
©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 -
©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