Optimizing Kotlin Multiplatform Testing: Building a Device-Independent Suite
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
CoreLibensures that use cases touch interfaces rather than platform SDKs, allowing for fast, deterministic tests incommonTest. - Reactive Stream Validation: Using Turbine with
runTestallows developers to assert onFlowandStateFlowemissions 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
End-to-End Password Reset Testing in Next.js with Playwright and ZeroDrop
Implement full E2E password reset testing in Next.js using Playwright and ZeroDrop to verify token generation, email delivery, and authentication.
Optimizing Cypress E2E Tests: Testing Real Email Flows Without Infrastructure
Eliminate Docker and MailHog from Cypress E2E tests using ZeroDrop for isolated, real email flow verification without mocking.
Playwright vs Selenium 2026: The Modern Test Automation Guide
Playwright reduces test flakiness to ~3% compared to Selenium's ~15% by using event-driven architecture and auto-waiting for modern SPAs.