Testing Coroutines & Flow Quiz

TESTING › Unit

Which dispatcher does runTest use by default when you do not pass one?

Answer: StandardTestDispatcher

runTest builds a TestScope backed by a StandardTestDispatcher by default, which queues launched coroutines for explicit time advancement.

Inside runTest using StandardTestDispatcher, you call launch { repo.save() } and immediately assert. What is the state of that coroutine before any time advance?

Answer: It is queued on the scheduler and has not run yet

StandardTestDispatcher queues new coroutines; they only run when you yield the test thread via advanceUntilIdle(), runCurrent(), or advanceTimeBy(). So an immediate assertion would see no effect yet.

How does UnconfinedTestDispatcher differ from StandardTestDispatcher?

Answer: It starts new coroutines eagerly until they first suspend

UnconfinedTestDispatcher executes new coroutines eagerly (no scheduling step) until they suspend, making simple tests terser; both dispatchers still use the same virtual-time scheduler.

For a local unit test of a ViewModel that launches work in viewModelScope, what must you do before the test runs?

Answer: Swap in Dispatchers.setMain(), then undo with resetMain()

viewModelScope uses Dispatchers.Main, which is unavailable on the JVM, so you swap it with a test dispatcher via Dispatchers.setMain() and undo it with resetMain() (typically via a MainDispatcherRule).

In Turbine, what does awaitComplete() assert?

Answer: That the flow terminated successfully without throwing

awaitComplete() suspends until the flow finishes normally (onComplete) with no exception; awaitError() is the counterpart for a Throwable termination.

A Turbine test{} block finishes while an emitted item and the Complete event are still unconsumed. What happens?

Answer: The test fails with a TurbineAssertionError for unconsumed events

Turbine enforces that every event is consumed; leftover items or completion at block end raise a TurbineAssertionError unless you explicitly call cancelAndIgnoreRemainingEvents().

Why is Turbine's test{} reliable for a MutableSharedFlow with replay = 0?

Answer: It starts collecting first, so emits inside the block are not dropped

With replay = 0, emissions before a collector subscribes are lost; Turbine's test{} starts collecting and waits until collection is active before running your block, so values emitted inside it are received.

Starting at virtual time 0, a child coroutine on StandardTestDispatcher calls delay(1000) and then sets a flag. You call advanceTimeBy(1000) and immediately read the flag. Is it set?

Answer: No; it skips a resume at exactly 1000, so the flag stays unset for now.

advanceTimeBy(n) advances virtual time and runs tasks scheduled strictly before currentTime + n; a continuation resuming at exactly that instant is left pending until you advance one more unit or call runCurrent().

You need to collect a hot StateFlow that never completes and assert its values without runTest hanging when the body returns. Where should the collector be launched?

Answer: In TestScope.backgroundScope, auto-cancelled when the test ends

runTest waits for everything in the main scope, so an endless collector there would hang it; backgroundScope exists for never-completing work and is auto-cancelled at the end of the test.

Work launched in runTest's main TestScope never completes (e.g. it awaits a signal that never arrives). What does runTest do?

Answer: It waits on its scope, then fails with an UncompletedCoroutinesError

runTest joins every coroutine started in its TestScope; if one cannot complete it eventually fails (after the default timeout) with an UncompletedCoroutinesError rather than passing or hanging indefinitely.

A repository does its work inside withContext(Dispatchers.IO) { ... } with the dispatcher hardcoded. Why does this hurt testability and what is the standard fix?

Answer: Hardcoding the dispatcher blocks the shared test scheduler; inject it instead

Dispatchers.IO is a real thread pool that does not share the virtual-time scheduler, so delays and ordering become nondeterministic; injecting the dispatcher lets the test pass a TestDispatcher tied to the same scheduler.

In setup you call Dispatchers.setMain(StandardTestDispatcher()), then inside the test you construct another StandardTestDispatcher() without passing a scheduler. Which scheduler does the second one use?

Answer: It uses the scheduler from setMain, so both share one virtual clock

After setMain installs a TestDispatcher, TestDispatchers created later with no explicit scheduler adopt Main's scheduler, keeping all coroutines on a single timeline so advancing time affects them together.

A ViewModel exposes uiState via stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Initial). In a test you advanceUntilIdle() then read uiState.value, but it is still Initial. Why?

Answer: WhileSubscribed waits for a collector, so collect uiState first

WhileSubscribed only runs the upstream flow while a subscriber is present; with no collector the source never executes, so the StateFlow stays at its initial value until you start collecting it.

A coroutine does some immediate work, then delay(10_000), then more work. After it is launched you call runCurrent() once. What executes?

Answer: Only the work at the current virtual time; delay(10_000) stays pending

runCurrent() runs tasks already scheduled at the present virtual time without moving the clock forward, so the pre-delay work runs but the continuation after delay(10_000) stays pending until time is advanced.

Back to Testing Coroutines & Flow