# KMP Coroutine Tests: Test Time Without Waiting for It

A test that sleeps for a second to verify a one-second delay is slow by design. A test that sleeps a little longer to avoid failing on CI is also difficult to trust.

For shared coroutine logic, virtual time gives you a better question: what should have happened before this deadline, and what should happen at it?

## Make the delayed behavior small

Here is a deliberately tiny piece of shared code:

```kotlin
import kotlinx.coroutines.delay

class DelayedRefresh(
    private val refresh: suspend () -> Unit,
) {
    suspend fun run() {
        delay(1_000)
        refresh()
    }
}
```

The delay is only an example. In a real feature, a product rule should justify it. The important design choice is that the class suspends in its caller's coroutine instead of creating an unrelated scope.

## Assert the boundary explicitly

Put this test in `commonTest` with `kotlin.test` and `kotlinx-coroutines-test` dependencies:

```kotlin
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

@OptIn(ExperimentalCoroutinesApi::class)
class DelayedRefreshTest {
    @Test
    fun refreshRunsAtTheDeadline() = runTest {
        var refreshes = 0
        val action = DelayedRefresh { refreshes++ }

        val job = launch { action.run() }
        runCurrent()

        advanceTimeBy(999)
        assertEquals(0, refreshes)

        advanceTimeBy(1)
        runCurrent()
        assertEquals(1, refreshes)

        job.join()
    }
}
```

The first `runCurrent()` starts the launched coroutine and reaches its delay. Advancing by 999 ms proves the work has not fired early. Advancing to 1,000 ms moves the clock to the deadline; `runCurrent()` executes work scheduled at that instant.

This distinction matters because `advanceTimeBy` does not itself run tasks scheduled exactly at the destination time. Treating clock movement and task execution as separate steps makes boundary assertions easier to read.

## Test cancellation as a feature

Add a second test to the same class:

```kotlin
@Test
fun cancellationPreventsRefresh() = runTest {
    var refreshes = 0
    val action = DelayedRefresh { refreshes++ }

    val job = launch { action.run() }
    runCurrent()
    advanceTimeBy(500)

    job.cancel()
    job.join()

    advanceTimeBy(1_000)
    runCurrent()
    assertEquals(0, refreshes)
}
```

A completed success test cannot prove that obsolete work stops. This cancellation test expresses a separate requirement: cancelling the owner before the deadline must prevent the refresh.

## Virtual time has a boundary too

The test scheduler controls work using its test dispatchers. It does not accelerate a real server, blocking sleep, or arbitrary work moved to a production dispatcher.

When a class must switch dispatcher, inject that dispatcher and use a `StandardTestDispatcher(testScheduler)` in the test. Keep related test dispatchers on the same scheduler. Otherwise, advancing one clock may not advance the work you intended to observe.

Keep `runTest` as the returned result of the test function, as shown above, for cross-platform compatibility. Avoid placing unrelated work after it.

## What this still does not prove

A passing common test validates the shared behavior on the targets where you actually run it. Merely placing a test in `commonTest` is not evidence that CI executed every target.

Discover the test tasks exposed by your module, run the relevant Android/JVM and native targets, and respect host requirements for Apple targets. Native integration and UI behavior deserve their own tests.

For timing-sensitive code, a useful test tells a short story: start work, advance to just before the boundary, assert, cross the boundary, assert again. That is more informative than waiting and hoping the final state appears.

## References

- [Kotlin: kotlinx-coroutines-test](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/)
- [Kotlin: advanceTimeBy](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/advance-time-by.html)
- [Kotlin: Test your multiplatform app](https://kotlinlang.org/docs/multiplatform/multiplatform-run-tests.html)

