When injecting a test dispatcher (e.g., into setMain or a provider), what changes if you choose StandardTestDispatcher vs UnconfinedTestDispatcher, and how does that interact with asserting ViewModel state right after launch?
answer
- Standard = queued/lazy -> must advance
- Unconfined = eager until first suspension
- Standard sees intermediate states (runCurrent)
- Unconfined runs synchronous part immediately
- Both share scheduler; advanceUntilIdle finishes suspends
basics
~10 sStandardTestDispatcher queues new coroutines and doesn't run them until you advance time, so you must call advanceUntilIdle before asserting. UnconfinedTestDispatcher runs them eagerly up to the first suspension, so state is often already set.
solid answer
~40 sBoth are `TestDispatcher`s sharing the test scheduler, but they differ in *when* launched coroutines start. `StandardTestDispatcher` is **queueing/lazy**: a `launch`/`async` body does not run at the launch point; you must yield control via `advanceUntilIdle()`, `advanceTimeBy()`, or `runCurrent()`. This forces explicit, deterministic ordering — good for testing intermediate states. `UnconfinedTestDispatcher` is **eager**: a launched coroutine runs immediately on the current thread until its first real suspension, so after `vm.load()` the synchronous part has already executed and state may already be updated without advancing. The injection choice therefore dictates your assertion style: with Standard you advance then assert; with Unconfined you can often assert directly but lose the ability to observe the pre-execution state. For ViewModels asserting a final state, either works if you advance; for asserting loading→loaded transitions, Standard gives finer control.
code
kotlin · 13 lines// Standard: must advance to run the launch
Dispatchers.setMain(StandardTestDispatcher())
val vm1 = VM(); vm1.load()
assertEquals(Initial, vm1.state.value) // not started yet
advanceUntilIdle()
assertEquals(Loaded, vm1.state.value)
// Unconfined: synchronous prefix already ran
Dispatchers.setMain(UnconfinedTestDispatcher())
val vm2 = VM(); vm2.load()
assertEquals(Loading, vm2.state.value) // ran up to first suspend
advanceUntilIdle()
assertEquals(Loaded, vm2.state.value)go deeper
Knows the two names exist; may not articulate the eager-vs-queued difference.
Explains Standard needs advanceUntilIdle and Unconfined runs eagerly to first suspension.
Maps the choice to assertion strategy (intermediate vs final state) and confirms both keep delay virtual via shared scheduler.
Sets a team convention (default Standard for determinism), explains pitfalls of Unconfined hiding ordering bugs, and edge cases with nested launches.
## The two test dispatchers Both come from `kotlinx-coroutines-test`, both implement `TestDispatcher`, and both can share the `TestCoroutineScheduler` from `runTest`. The difference is **eagerness of newly launched coroutines**. ### StandardTestDispatcher (default in runTest) - New coroutines from `launch`/`async` are **enqueued**, not started, at the call site. - Nothing runs until you give the scheduler a chance: `advanceUntilIdle()` (run everything until idle), `advanceTimeBy(ms)` (run up to a virtual time), or `runCurrent()` (run tasks scheduled at the current time). - Deterministic, queue-based ordering; lets you observe intermediate states. ### UnconfinedTestDispatcher - A launched coroutine starts **immediately and eagerly** on the current call stack, running until its first genuine suspension point. - Less ceremony — state set synchronously before the first `delay`/suspend is visible right away. - You give up the ability to see the state *before* that synchronous work ran. ## Interaction with ViewModel assertions Consider: ```kotlin fun load() = viewModelScope.launch { _state.value = Loading val data = repo.fetch() // suspends _state.value = Loaded(data) } ``` **With StandardTestDispatcher (injected via setMain):** ```kotlin Dispatchers.setMain(StandardTestDispatcher()) val vm = VM(); vm.load() // nothing has run yet: assertEquals(Initial, vm.state.value) advanceUntilIdle() assertEquals(Loaded(data), vm.state.value) ``` You can also `runCurrent()` to reach `Loading` before `fetch()` completes. **With UnconfinedTestDispatcher:** ```kotlin Dispatchers.setMain(UnconfinedTestDispatcher()) val vm = VM(); vm.load() // the launch already ran up to repo.fetch(): assertEquals(Loading, vm.state.value) advanceUntilIdle() // still needed to finish the suspended part assertEquals(Loaded(data), vm.state.value) ``` ## Guidance - Choose **Standard** when you want explicit control and to assert ordered intermediate states; it matches `runTest`'s default scheduler behavior. - Choose **Unconfined** when you just want fire-and-forget background work to have run, reducing `advance` calls — common for simple ViewModel tests collecting `StateFlow`. - Either way, **share the scheduler** so virtual time and `delay()` skipping work; `advanceUntilIdle()` still finishes any suspended coroutines regardless of dispatcher. ## Key APIs/terms - **`StandardTestDispatcher`**, **`UnconfinedTestDispatcher`**, **`TestDispatcher`**. - **`advanceUntilIdle()`**, **`advanceTimeBy()`**, **`runCurrent()`**. - **`viewModelScope` / `StateFlow`** — typical subjects of these tests.
- Which dispatcher does runTest use by default for its body and child coroutines?StandardTestDispatcher — so child coroutines are queued and you must advance (or runCurrent) to run them.
- Does UnconfinedTestDispatcher make delay() run in real time?No. It only changes start eagerness; delay() is still virtual because it shares the TestCoroutineScheduler, so advancing skips it.
saying these in an interview costs you the question
- Thinks UnconfinedTestDispatcher makes delay real
- Asserts ViewModel state after launch with Standard but never advances
- Believes the two dispatchers differ in thread pools rather than start eagerness
- Claims Standard can never observe intermediate states (forgets runCurrent)
- Uses Unconfined to 'avoid testing' suspension/ordering entirely