Skip to main content

On This Page

Optimizing Kotlin Multiplatform Testing: Building a Device-Independent Suite

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

The fastest test suite is the one that doesn’t need a device

The AX code outlines a testing pyramid for Kotlin Multiplatform (KMP) designed to minimize device dependency. By utilizing a framework-free core, the majority of logic is verified in commonTest without emulators or simulators.

Why This Matters

In typical mobile development, reliance on emulators and physical devices introduces flakiness and significant latency into the CI/CD pipeline. While the ideal model suggests full end-to-end validation, the technical reality is that device-based tests are slow; shifting verification to a hexagonal core ensures deterministic results and immediate developer feedback, reducing the cost of build gates.

Key Insights

  • Hexagonal Architecture: Isolating domain logic in CoreLib ensures that use cases touch interfaces rather than platform SDKs, allowing for fast, deterministic tests in commonTest.
  • Reactive Stream Validation: Using Turbine with runTest allows developers to assert on Flow and StateFlow emissions deterministically without manual polling.
  • Architectural Guardrails: ArchUnit allows teams to write assertions that fail the build if platform dependencies leak into the domain layer, preventing boundary erosion.
  • Headless UI Verification: Paparazzi enables Compose UI testing by rendering composables to images and diffing them against goldens on the JVM, removing the need for an emulator.

Working Examples

Pure domain test running in commonTest across all targets.

class PlayabilityCalculatorTest {
@Test
fun penalizesHighWind() {
val ctx = scoringContext(windMph = 35, tempF = 68, rain = false)
val score = PlayabilityCalculator().calculateScore(ctx)
assertTrue(score.value < 50)
}
}

Testing reactive flows using Turbine.

@Test
fun emitsConnectingThenConnected() = runTest {
bleClient.connectionState.test {
assertEquals(DISCONNECTED, awaitItem())
bleClient.connect("device-1")
assertEquals(CONNECTING, awaitItem())
assertEquals(CONNECTED, awaitItem())
cancelAndIgnoreRemainingEvents()
}
}

Enforcing architectural boundaries with ArchUnit.

@Test
fun domainHasNoPlatformDependencies() {
classes().that().resideInAPackage("..core.domain..")
.should().onlyDependOnClassesThat()
.resideOutsideOfPackages("android..", "platform..", "java.net..")
.check(importedClasses)
}

Practical Applications

  • ), 90%+ of behavior is verified in commonTest using fakes instead of SDK mocks.
  • ), failing builds when coverage drops below specific thresholds (e.g., 80%) using Kover.

References:

  • From internal analysis

Continue reading

Next article

Wanaku 0.1.1: Scaling AI Agent Capabilities with Apache Camel and MCP

Related Content