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.

APIUse
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
currentTimecurrent virtual time in ms
backgroundScopescope 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)
}