skip to content

How do you write shared tests for a KMP module, and what role does `commonTest` and the `kotlin.test` library play?

level: middleimportance: must knowfreq 55%

answer

  1. commonTest mirrors commonMain, depends on it
  2. kotlin("test") dependency
  3. @Test/@BeforeTest + assertEquals/assertFailsWith
  4. Maps to JUnit / Native runner / JS per target
  5. allTests vs jvmTest

basics

~10 s

Put shared tests in commonTest and use the kotlin.test library. Those tests run once per target, so a single test verifies behavior on JVM, iOS, JS, etc.

solid answer

~40 s

`commonTest` is the root *test* source set, the mirror of `commonMain`. Tests written there are compiled and executed for every target, so one test suite validates shared logic everywhere. You use the multiplatform `kotlin.test` library, which provides target-agnostic annotations and assertions — `@Test`, `@BeforeTest`, `@AfterTest`, `@Ignore`, plus `assertEquals`, `assertTrue`, `assertFailsWith`, `assertNull`. At build time `kotlin.test` maps onto each platform's real runner (JUnit on JVM, Kotlin/Native's test runner, etc.). Add it with `commonTest.dependencies { implementation(kotlin("test")) }`. `commonTest` depends on `commonMain` automatically, so it sees all common declarations. Platform-specific tests can still live in `jvmTest`, `iosTest`, etc., for behavior you can only assert on one platform.

code

kotlin · 16 lines
kotlin
// build.gradle.kts
kotlin {
    sourceSets {
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

// src/commonTest/kotlin/CalcTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals

class CalcTest {
    @Test fun adds() = assertEquals(4, 2 + 2)
}

go deeper

for a junior

Knows shared tests go in commonTest and uses kotlin.test annotations.

for a middle

Explains per-target execution, the kotlin("test") dependency, and that kotlin.test maps onto each platform's runner.

for a senior

Discusses when to drop to platform test source sets and how coroutine tests (runTest) work in common.

for a principal

Designs a testing strategy maximizing common coverage while reserving platform tests for genuinely platform-bound behavior, and reasons about CI matrix cost across targets.

## `commonTest`: the shared test source set Every KMP module has a parallel test hierarchy mirroring the production one. `commonTest` (`src/commonTest/kotlin`) is the root test source set and is the test-side counterpart of `commonMain`. It **automatically depends on `commonMain`**, so all your shared declarations are visible. Tests written here are compiled and run **once per target**, meaning a single assertion is verified on the JVM, on iOS, on JS — anywhere your module compiles. ## The `kotlin.test` library `kotlin.test` is the multiplatform testing API. It is added with the `kotlin("test")` helper: ```kotlin kotlin { sourceSets { commonTest.dependencies { implementation(kotlin("test")) } } } ``` It provides **target-agnostic** annotations and assertions: - **Annotations:** `@Test`, `@BeforeTest`, `@AfterTest`, `@Ignore`, `@BeforeClass`/`@AfterClass` equivalents per platform. - **Assertions:** `assertEquals`, `assertNotEquals`, `assertTrue`, `assertFalse`, `assertNull`, `assertNotNull`, `assertSame`, `assertFailsWith<T>`, `fail()`. Under the hood, `kotlin.test` **maps to the real runner of each platform**: JUnit (4 or 5) on the JVM, the Kotlin/Native test infrastructure on native targets, and the relevant JS test framework on JS. You write once against `kotlin.test`; the build wires the correct backend. ```kotlin // src/commonTest/kotlin/GreetingTest.kt import kotlin.test.Test import kotlin.test.assertTrue class GreetingTest { @Test fun greetingContainsHello() { val result = Greeting().greet() assertTrue(result.contains("Hello")) } } ``` ## Running them - `./gradlew allTests` runs tests for every target. - `./gradlew jvmTest`, `./gradlew iosSimulatorArm64Test`, etc. run a single target's tests (including the common ones compiled into it). ## When to use platform test source sets If an assertion needs a platform API (e.g. checking a `java.io.File` was written on JVM, or a `NSDate` on iOS), put it in `jvmTest`, `iosTest`, etc. Those depend on their `*Main` counterpart and may use platform APIs and platform-specific test frameworks. ## Coroutines in tests With `kotlinx-coroutines-test` added to `commonTest`, you can use `runTest { ... }` to drive `suspend` functions deterministically in common tests. ## Key takeaways - `commonTest` mirrors `commonMain`, runs once per target. - Use `kotlin.test` annotations/assertions for portability. - The build maps `kotlin.test` to each platform's real runner. - Platform-only assertions go in `jvmTest`/`iosTest`/etc.

  • What does `kotlin.test` map to on the JVM?
    A JUnit backend (JUnit 4 or 5, configurable). The `@Test`/assert calls are routed to JUnit's runner so JVM tests appear as normal JUnit tests.
  • Where would a test that asserts on `java.io.File` behavior belong?
    In `jvmTest`, not `commonTest`, because `java.io.File` is JVM-only and common tests must compile for every target.

saying these in an interview costs you the question

  • Using JUnit's `org.junit.Test` directly in `commonTest`
  • Thinking common tests run only once total instead of per target
  • Not knowing the `kotlin("test")` dependency helper
  • Believing platform-specific assertions can live in `commonTest`
  • Confusing `commonTest` with an integration-test module

context