Skip to main content

Kotlin Multiplatform Testing

Introduction

Testing is a critical part of any software development process, and Kotlin Multiplatform (KMP) projects are no exception. In fact, testing becomes even more important when your code needs to run on multiple platforms. Kotlin Multiplatform allows you to share tests across platforms, helping you ensure that your shared code behaves consistently regardless of where it runs.

In this guide, you'll learn:

  • How testing works in Kotlin Multiplatform projects
  • How to set up tests for different platforms
  • How to write common tests that run on all platforms
  • How to write platform-specific tests
  • Best practices for testing Kotlin Multiplatform code

Testing Fundamentals in Kotlin Multiplatform

Kotlin Multiplatform allows you to write tests in three different source sets:

  1. Common tests: Tests written in the common source set that run on all platforms
  2. Platform-specific tests: Tests written for specific platforms (e.g., JVM, iOS, JS)
  3. Integration tests: Tests that verify the integration between common code and platform-specific code

Let's see how to set up and use each type.

Setting Up Your Project for Testing

Project Structure

A typical Kotlin Multiplatform project structure with testing might look like this:

src/
commonMain/ # Common code
commonTest/ # Common tests
jvmMain/ # JVM-specific code
jvmTest/ # JVM-specific tests
iosMain/ # iOS-specific code
iosTest/ # iOS-specific tests
jsMain/ # JavaScript-specific code
jsTest/ # JavaScript-specific tests

Adding Testing Dependencies

Here's how to set up testing dependencies in your build.gradle.kts file:

kotlin
kotlin {
sourceSets {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-annotations-common"))
}
}

val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}

val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}

val iosTest by getting {
// iOS tests use the common test library
}
}
}

Writing Common Tests

Common tests are written in the commonTest source set and run on all platforms. Let's create a simple example.

First, let's create a common function to test:

kotlin
// In commonMain
package com.example.calculator

expect class PlatformInfo() {
fun getName(): String
}

class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun subtract(a: Int, b: Int): Int = a - b
fun multiply(a: Int, b: Int): Int = a * b
fun divide(a: Int, b: Int): Int {
require(b != 0) { "Cannot divide by zero" }
return a / b
}
}

Now, let's write a test for this calculator in the common source set:

kotlin
// In commonTest
package com.example.calculator

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class CalculatorTest {
private val calculator = Calculator()

@Test
fun testAddition() {
assertEquals(4, calculator.add(2, 2))
assertEquals(0, calculator.add(0, 0))
assertEquals(-2, calculator.add(-1, -1))
}

@Test
fun testSubtraction() {
assertEquals(0, calculator.subtract(2, 2))
assertEquals(5, calculator.subtract(10, 5))
assertEquals(-5, calculator.subtract(5, 10))
}

@Test
fun testMultiplication() {
assertEquals(4, calculator.multiply(2, 2))
assertEquals(0, calculator.multiply(0, 5))
assertEquals(-10, calculator.multiply(2, -5))
}

@Test
fun testDivision() {
assertEquals(1, calculator.divide(2, 2))
assertEquals(2, calculator.divide(10, 5))

assertFailsWith<IllegalArgumentException> {
calculator.divide(10, 0)
}
}
}

These tests will run on all platforms and ensure the calculator works the same way everywhere.

Writing Platform-Specific Tests

For platform-specific tests, you create test files in the corresponding test source set. Let's implement the PlatformInfo class for each platform and test it:

kotlin
// In jvmMain
package com.example.calculator

actual class PlatformInfo {
actual fun getName(): String = "JVM"
}

// In iosMain
package com.example.calculator

actual class PlatformInfo {
actual fun getName(): String = "iOS"
}

// In jsMain
package com.example.calculator

actual class PlatformInfo {
actual fun getName(): String = "JS"
}

Now let's write platform-specific tests:

kotlin
// In jvmTest
package com.example.calculator

import kotlin.test.Test
import kotlin.test.assertEquals

class PlatformInfoTest {
@Test
fun testPlatformName() {
val platformInfo = PlatformInfo()
assertEquals("JVM", platformInfo.getName())
}
}

You would create similar files in iosTest and jsTest source sets, with the appropriate expected platform name.

Using Expect/Actual for Testing

Sometimes you need to test behavior that varies between platforms. You can use expect/actual to handle this:

kotlin
// In commonTest
package com.example.utils

import kotlin.test.Test

expect class FileHelper() {
fun fileExists(path: String): Boolean
}

class FileUtilsTest {
@Test
fun testFileOperations() {
val fileHelper = FileHelper()
// Common test logic here
}
}

// In jvmTest
package com.example.utils

import java.io.File

actual class FileHelper {
actual fun fileExists(path: String): Boolean {
return File(path).exists()
}
}

// In jsTest
package com.example.utils

actual class FileHelper {
actual fun fileExists(path: String): Boolean {
// JS-specific implementation
return false // Simplified for example
}
}

Running Tests

To run tests for all platforms:

bash
./gradlew allTests

To run tests for a specific platform:

bash
./gradlew jvmTest     # For JVM tests
./gradlew iosTest # For iOS tests
./gradlew jsTest # For JavaScript tests

Real-World Example: Testing a Multiplatform API Client

Let's consider a more practical example of testing a multiplatform API client. We'll create a simple API client that fetches user data:

kotlin
// In commonMain
package com.example.api

expect class HttpClient() {
suspend fun get(url: String): String
}

class UserApiClient(private val httpClient: HttpClient) {
suspend fun getUser(id: String): User {
val response = httpClient.get("https://api.example.com/users/$id")
return parseUserJson(response)
}

private fun parseUserJson(json: String): User {
// In a real app, you would use a JSON library here
// Simplified for the example
val name = json.substringAfter("\"name\":\"").substringBefore("\"")
val email = json.substringAfter("\"email\":\"").substringBefore("\"")
return User(name, email)
}
}

data class User(val name: String, val email: String)

Now, let's write tests for this API client using a mock HTTP client:

kotlin
// In commonTest
package com.example.api

import kotlin.test.Test
import kotlin.test.assertEquals

class MockHttpClient : HttpClient() {
var responseToReturn: String = ""
var lastUrl: String? = null

override suspend fun get(url: String): String {
lastUrl = url
return responseToReturn
}
}

class UserApiClientTest {
@Test
fun testGetUser() {
val mockClient = MockHttpClient()
mockClient.responseToReturn = """{"name":"John Doe","email":"[email protected]"}"""

val apiClient = UserApiClient(mockClient)
val user = runTest { apiClient.getUser("123") }

assertEquals("John Doe", user.name)
assertEquals("[email protected]", user.email)
assertEquals("https://api.example.com/users/123", mockClient.lastUrl)
}
}

// Helper function to run suspend functions in tests
expect fun <T> runTest(block: suspend () -> T): T

You would then provide the platform-specific implementations of HttpClient and the runTest function in their respective source sets.

Best Practices for Kotlin Multiplatform Testing

  1. Share test logic whenever possible: Put as much test code as you can in the common source set to ensure consistent behavior across platforms.

  2. Test platform-specific implementations separately: Make sure to test the actual implementations for each platform.

  3. Use mocks for external dependencies: As shown in the API client example, create mock implementations for testing.

  4. Test both success and failure cases: Always include tests for error conditions and edge cases.

  5. Keep platform-specific tests focused: When writing platform-specific tests, focus only on the platform-specific behavior.

  6. Use parametrized tests: For common tests that need slightly different behavior on different platforms, consider using parametrized tests.

  7. Make your tests readable: Write clear test names and use appropriate assertions to make your tests self-documenting.

Summary

Testing Kotlin Multiplatform code is essential to ensure your shared logic works correctly across all target platforms. In this guide, you've learned:

  • How to structure your project for testing
  • How to write common tests that run on all platforms
  • How to write platform-specific tests
  • How to use mocks and test helpers for more complex scenarios
  • Best practices for effective multiplatform testing

Remember that thorough testing is one of the key advantages of using Kotlin Multiplatform—it helps you catch platform-specific issues early and ensures consistent behavior of your shared code.

Additional Resources

Exercises

  1. Create a simple Kotlin Multiplatform project and write common tests for a string utility class with methods like reverse(), capitalize(), and countOccurrences().

  2. Implement an expect class FileStorage with methods for saving and retrieving data, and provide actual implementations for at least two platforms. Write tests for both common behavior and platform-specific functionality.

  3. Create a multiplatform networking client that handles different types of HTTP responses and write tests using mocks to verify it processes both successful and error responses correctly.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)