skip to content

How is runBlocking used in tests, and why is runTest usually preferred?

level: seniorimportance: should knowfreq 55%

answer

  1. runBlocking = real time; runTest = virtual time
  2. runTest skips/auto-advances delay
  3. TestScope + TestCoroutineScheduler
  4. advanceTimeBy / advanceUntilIdle / runCurrent
  5. Don't nest runBlocking inside runTest

basics

~10 s

runBlocking lets a normal test method call suspend functions by waiting for them. But it really sleeps through delays, making tests slow. runTest fast-forwards virtual time, so tests run instantly with more control.

solid answer

~40 s

A JUnit test method is a regular function, so to call suspend code you need a bridge. `runBlocking { ... }` works: it blocks the test thread until the suspend body finishes, and assertions run normally. The downside is that `delay` and time-based logic are real: a `delay(10_000)` actually waits ten seconds, making coroutine tests slow and flaky. `runTest` from `kotlinx-coroutines-test` solves this. It runs on a `TestScope` backed by a `TestCoroutineScheduler` with **virtual time**: `delay` is skipped/auto-advanced, so a test that models long waits finishes near-instantly. It also gives you control — `advanceTimeBy`, `advanceUntilIdle`, `runCurrent`, a `StandardTestDispatcher` (queued) vs `UnconfinedTestDispatcher` (eager) — and integrates with `Dispatchers.setMain` for ViewModel-style code. So: runBlocking for trivial/interop cases or quick scripts; runTest as the default for testing suspend/coroutine behavior.

code

kotlin · 18 lines
kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

class DebounceTest {
    @Test fun emitsAfterQuietPeriod() = runTest {
        var emitted = false
        val job = launch { delay(1_000); emitted = true }
        advanceTimeBy(999)
        assertEquals(false, emitted)   // not yet
        advanceTimeBy(1)
        runCurrent()
        assertEquals(true, emitted)    // virtual time, instant wall-clock
        job.join()
    }
}

go deeper

for a junior

Knows runBlocking lets a test call suspend functions and waits for the result.

for a middle

Can explain runTest skips real delays via virtual time and is the usual default.

for a senior

Drives the scheduler (advanceTimeBy/advanceUntilIdle), chooses Standard vs Unconfined dispatchers, and uses setMain for ViewModels.

for a principal

Establishes test-strategy conventions (virtual time everywhere, no runBlocking in suspend tests, deterministic dispatchers) across the codebase.

## Why a bridge is needed in tests A test method like `@Test fun foo() { ... }` is **not** a suspend function, yet the code under test often is. You need a coroutine builder callable from non-suspend code. Both `runBlocking` and `runTest` qualify. ## runBlocking in tests ```kotlin import kotlinx.coroutines.* import kotlin.test.Test import kotlin.test.assertEquals class RepoTest { @Test fun loadsUser() = runBlocking { val user = repo.load(1) // suspend call, bridged assertEquals("Ada", user.name) } } ``` - Blocks the test thread until the body completes; assertions and exceptions behave normally. - **Real time**: `delay(5_000)` truly waits 5 seconds. Code with timeouts, retries with backoff, or polling becomes slow and non-deterministic. ## runTest — the preferred tool `runTest` from the `kotlinx-coroutines-test` artifact runs the body in a **`TestScope`** whose dispatcher shares a **`TestCoroutineScheduler`** providing **virtual time**. ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class TimerTest { @Test fun completesInstantly() = runTest { val r = withTimeoutOrNullModel() // internally delays 10s assertEquals("done", r) // wall-clock: milliseconds, not 10 seconds } } ``` Key advantages: - **Virtual time**: `delay` doesn't really sleep; the scheduler auto-advances when the test is otherwise idle, so long waits cost nothing. - **Time control**: `testScheduler.advanceTimeBy(ms)`, `advanceUntilIdle()`, `runCurrent()` let you drive time deterministically and assert intermediate states. - **Dispatchers**: `StandardTestDispatcher` queues new coroutines (you advance time to run them — good for ordering assertions); `UnconfinedTestDispatcher` runs them eagerly until first suspension. - **Main dispatcher**: pair with `Dispatchers.setMain(dispatcher)` / `resetMain()` to test code that uses `Dispatchers.Main` (e.g. Android ViewModels). - **Uncaught exceptions / un-joined children** are surfaced as test failures. ## When each is appropriate - **runBlocking**: tiny scripts, `main()`, interop, or trivial suspend calls with no time dependence. - **runTest**: the default for anything testing coroutine timing, concurrency, flows, timeouts, or ViewModels. ## Common pitfall Don't nest `runBlocking` inside a `runTest` body to 'force' something — it blocks the test thread and breaks virtual-time control. Stay within `runTest` and use scheduler controls. ## Keywords / APIs `runBlocking`, `runTest`, `TestScope`, `TestCoroutineScheduler`, `StandardTestDispatcher`, `UnconfinedTestDispatcher`, `advanceTimeBy`, `advanceUntilIdle`, `runCurrent`, `Dispatchers.setMain`/`resetMain`, virtual time.

  • What does virtual time mean in runTest?
    The TestCoroutineScheduler tracks a fake clock; delay registers a resumption at that fake time and is auto-advanced when idle, so no real wall-clock waiting occurs.
  • What's the difference between StandardTestDispatcher and UnconfinedTestDispatcher?
    Standard queues newly launched coroutines until you advance time/run the scheduler; Unconfined runs them eagerly up to their first suspension point.

saying these in an interview costs you the question

  • Claiming runBlocking fast-forwards delays like runTest
  • Defaulting to runBlocking for timing/flow tests and accepting slow suites
  • Nesting runBlocking inside runTest
  • Not knowing about TestCoroutineScheduler / advanceUntilIdle
  • Forgetting Dispatchers.setMain for Main-dispatcher code

context