skip to content

In kotlinx-coroutines-test, what are StandardTestDispatcher and UnconfinedTestDispatcher, and how do they differ in when they execute a newly launched coroutine?

level: juniorimportance: must knowfreq 62%

answer

  1. Standard = queued, run nothing until you advance
  2. Unconfined = eager, runs to first suspension
  3. Both share one TestCoroutineScheduler / virtual clock
  4. runTest defaults to StandardTestDispatcher
  5. Unconfined mirrors Dispatchers.Unconfined but fake time

basics

~10 s

Both are test dispatchers using fake time. StandardTestDispatcher waits: a new coroutine just gets queued until you advance time. UnconfinedTestDispatcher is eager: it runs the new coroutine immediately until its first suspension point.

solid answer

~40 s

Both are TestDispatcher implementations from kotlinx-coroutines-test that share a TestCoroutineScheduler controlling virtual time. StandardTestDispatcher does not run launched coroutines eagerly: when you call launch {} or async {}, the body is only scheduled on the scheduler and won't start until the scheduler is told to run pending tasks (via advanceUntilIdle(), runCurrent(), or runTest finishing). This gives deterministic, fully manual control of ordering. UnconfinedTestDispatcher runs each new coroutine eagerly on the current thread up to its first real suspension point (the first place it actually suspends and yields), like the production Dispatchers.Unconfined, but it still uses the virtual clock for delay(). By default runTest uses StandardTestDispatcher. Use UnconfinedTestDispatcher when you want launched work to have already started without manually advancing time.

go deeper

for a junior

States the core contrast: Standard queues, Unconfined runs eagerly to first suspension.

for a middle

Adds that both share a TestCoroutineScheduler and that runTest defaults to Standard.

for a senior

Explains 'first suspension point' precisely and ties Unconfined to production Dispatchers.Unconfined semantics.

for a principal

Frames the choice as a determinism-vs-ergonomics trade-off and discusses how it affects observability of intermediate state.

## What is a TestDispatcher? A *dispatcher* in coroutines decides on which thread, and *when*, a coroutine's code runs. `kotlinx-coroutines-test` provides `TestDispatcher`, a special `CoroutineDispatcher` that uses a **virtual clock** instead of the real wall clock. That clock is the `TestCoroutineScheduler`. Because time is fake, `delay(10_000)` does not actually pause your test for 10 seconds; it just moves the virtual clock forward when you tell it to. ## The shared scheduler A `TestCoroutineScheduler` holds the queue of pending coroutine continuations and the current virtual time. **Both** `StandardTestDispatcher` and `UnconfinedTestDispatcher` can be built on the *same* scheduler instance, so they share one timeline. `runTest` creates one scheduler and exposes it via `testScheduler`. ```kotlin val scheduler = TestCoroutineScheduler() val standard = StandardTestDispatcher(scheduler) val unconfined = UnconfinedTestDispatcher(scheduler) ``` ## StandardTestDispatcher — lazy / queued When you `launch {}` or `async {}` on a `StandardTestDispatcher`, the coroutine body is **not started immediately**. It is placed in the scheduler's queue. Nothing runs until you actively drive the scheduler: - `runCurrent()` runs tasks scheduled at the *current* virtual time. - `advanceUntilIdle()` runs everything until no more tasks remain. - `advanceTimeBy(ms)` moves the clock and runs tasks due in that window. - `runTest { ... }` automatically calls `advanceUntilIdle()` at the end. This makes ordering **fully deterministic and manual** — ideal when you want to assert intermediate state before background work runs. ## UnconfinedTestDispatcher — eager A coroutine launched on an `UnconfinedTestDispatcher` starts running **eagerly and immediately**, on the calling thread, up to its **first suspension point** — the first time it actually suspends (e.g. hits a real `delay`, awaits something not yet ready, or yields). After that first suspension, further resumption still goes through the scheduler/virtual clock. This mirrors production `Dispatchers.Unconfined`. ## Why it matters With `StandardTestDispatcher`, code right after a `launch` sees the launched coroutine as **not yet run**. With `UnconfinedTestDispatcher`, that same code sees the launched coroutine as **already executed up to its first suspension**. Choosing wrong leads to flaky or surprising assertions. ## Default `runTest` uses `StandardTestDispatcher` by default. Pass `UnconfinedTestDispatcher(testScheduler)` (or call `runTest(UnconfinedTestDispatcher())`) when eager start is desired.

  • Which one does runTest use by default?
    StandardTestDispatcher — launched coroutines are queued, not started, until time is advanced or runTest completes.
  • Do they share virtual time?
    Yes, when constructed from the same TestCoroutineScheduler they share one virtual clock and one task queue.

saying these in an interview costs you the question

  • Claiming StandardTestDispatcher runs launched coroutines immediately
  • Saying UnconfinedTestDispatcher uses real wall-clock time
  • Thinking the two dispatchers can't share a scheduler
  • Confusing 'first suspension point' with 'completion'
  • Believing runTest defaults to Unconfined

context