Shared code is worth exactly as much as your confidence in it. OrderRepository ships to Android and iOS, so it has to be proven on both. kotlin.test, plus the multiplatform kotlinx-coroutines-test artifact, lets one test class in commonTest run against every target's own test runner.
#The old way
OrderRepositoryTest existed twice. A JUnit test on Android, a separate XCTest suite on iOS, each hand-writing its own fake OrdersApi and asserting the same mapping behavior.
So a regression in the shared logic could pass on one platform's suite and fail on the other's. Or worse, get duplicated incorrectly on both.
#The new way
interface OrdersApi {
suspend fun fetchOrders(): List<Order>
}
data class Order(val id: String, val status: String, val totalCents: Int)
class OrderRepository(private val api: OrdersApi) {
suspend fun fetchOrders(): List<Order> = api.fetchOrders()
}
class FakeOrdersApi : OrdersApi {
var ordersToReturn: List<Order> = emptyList()
override suspend fun fetchOrders(): List<Order> = ordersToReturn
}
class OrderRepositoryTest {
@Test
fun fetchOrdersReturnsSeededResults() = runTest {
val fakeApi = FakeOrdersApi().apply {
ordersToReturn = listOf(Order(id = "1", status = "PAID", totalCents = 4200))
}
val repository = OrderRepository(fakeApi)
val result = repository.fetchOrders()
assertEquals(1, result.size)
assertEquals("PAID", result.first().status)
}
}
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
}
}
./gradlew allTests
allTests does both jobs from that one class. For androidUnitTest it runs OrderRepositoryTest on the JVM via JUnit; for each iosSimulatorArm64Test-style target it compiles the same code to a native binary that executes directly.
#Why it matters
- One test class validates the mapping logic on every platform it actually ships to
- Native targets catch Kotlin/Native-specific bugs (initialization order, native memory issues) that a JVM-only suite can't see
- Fakes like
FakeOrdersApilive incommonTesttoo, so test doubles don't fork the way production code used to
#Gotcha
runTest fast-forwards virtual time for any delay() inside the coroutines it controls. Outside that scope it does nothing. Thread.sleep on the JVM side, or a platform timer reached through native interop, still blocks for real.
A test that mixes the two doesn't fail outright. It runs slower and less predictably than the virtual-time parts suggest, which is easy to misdiagnose as flakiness in the production code instead of the test itself.