Careeroza
Testing Coroutines with runTest
Virtual Time
runTest { } from kotlinx-coroutines-test runs suspend code in a test. It uses virtual time: delay(10_000) finishes instantly, so tests with retries and timeouts stay fast and deterministic.
| API | Use |
|---|---|
runTest { } | test body for suspend code |
StandardTestDispatcher() | coroutines run only when the scheduler advances (precise control) |
UnconfinedTestDispatcher() | coroutines start eagerly (simple tests, collecting flows) |
advanceTimeBy(ms), advanceUntilIdle(), runCurrent() | move virtual time |
currentTime | current virtual time in ms |
backgroundScope | scope for never-ending collectors, cancelled at test end |
Dispatchers.setMain(...) | replace Dispatchers.Main (needed for ViewModels) |
Example
class RetryTest {
@Test
fun `retries three times with backoff`() = runTest {
var calls = 0
val result = retry(times = 4, initialDelay = 1_000) {
calls++
if (calls < 3) throw IOException("timeout")
"ok"
}
assertEquals("ok", result)
assertEquals(3, calls)
assertEquals(3_000, currentTime) // 1 s + 2 s of virtual time, test took ~5 ms
}
@Test
fun `withTimeoutOrNull returns null for a slow call`() = runTest {
val value = withTimeoutOrNull(500) { delay(2_000); "late" }
assertNull(value)
}
}Injecting Dispatchers
Code that hard-codes Dispatchers.IO escapes virtual time. Inject the dispatcher and pass the test dispatcher in tests:
class ReportRepository(private val io: CoroutineDispatcher = Dispatchers.IO) {
suspend fun build(): String = withContext(io) { delay(5_000); "report" }
}
@Test
fun `report builds in virtual time`() = runTest {
val repo = ReportRepository(io = StandardTestDispatcher(testScheduler))
assertEquals("report", repo.build())
assertEquals(5_000, currentTime)
}