Testing Coroutines & Flow Quiz
TESTING › Unit
Which dispatcher does runTest use by default when you do not pass one?
- UnconfinedTestDispatcher
- Dispatchers.Main
- StandardTestDispatcher
- Dispatchers.Default
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?
- It is queued on the scheduler and has not run yet
- It already ran eagerly on the current thread already
- It executed on a real background thread pool instead
- It threw because launch is forbidden in tests here
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?
- It runs delays in real wall-clock time instead of virtual time
- It disables the shared TestCoroutineScheduler entirely
- It requires an advanceUntilIdle() call to start any coroutine
- It starts new coroutines eagerly until they first suspend
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?
- Call advanceUntilIdle() right at the very start of the test
- Swap in Dispatchers.setMain(), then undo with resetMain()
- Run the test inside a runBlocking block instead of runTest
- Move all the coroutines onto TestScope.backgroundScope first
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?
- That the flow emitted at least one item
- That the flow cancelled before completing
- That the flow terminated successfully without throwing
- That no further items will ever be emitted but the flow stays active
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?
- The test fails with a TurbineAssertionError for unconsumed events
- The extra events are silently dropped, so the test still passes cleanly
- awaitItem() keeps retrying until those leftover events are read
- The collected flow restarts from the beginning after the block ends
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?
- It permanently buffers each emission, even when no collector is active
- It converts the hot flow into a cold one before the test block runs
- It raises the replay cache automatically to keep earlier emits around
- It starts collecting first, so emits inside the block are not dropped
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?
- Yes, advanceTimeBy(1000) will resume any delay(1000) right away.
- No; it skips a resume at exactly 1000, so the flag stays unset for now.
- No, it only shifts virtual time and never executes queued coroutine work.
- Yes, but only if you use UnconfinedTestDispatcher instead of StandardTestDispatcher.
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?
- In GlobalScope, so it keeps running after the test finishes
- In the main TestScope with launch {}, then cancel it manually yourself
- Inside a nested runBlocking block started from the test body
- In TestScope.backgroundScope, auto-cancelled when the test ends
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?
- It silently passes, simply ignoring the still-incomplete coroutine
- It hangs forever, never reporting any error back to the runner
- It waits on its scope, then fails with an UncompletedCoroutinesError
- It quietly moves the coroutine to backgroundScope and cancels it there
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?
- Hardcoding the dispatcher blocks the shared test scheduler; inject it instead
- It is fine; Dispatchers.IO automatically follows runTest's virtual time
- withContext is forbidden in tests, so the code should be rewritten with runBlocking
- Use Dispatchers.setIO() in the test to swap out the IO dispatcher
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?
- It gets a brand-new scheduler, so the two dispatchers run on separate clocks
- It has no scheduler at all, and creating it without one throws an error
- It falls back to the Dispatchers.Default scheduler for its timeline
- It uses the scheduler from setMain, so both share one virtual clock
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?
- stateIn always stays at its initial value in tests, so it never changes
- WhileSubscribed waits for a collector, so collect uiState first
- advanceUntilIdle() cancels the upstream before it has a chance to emit
- Only SharingStarted.Lazily works with runTest; WhileSubscribed is unsupported
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?
- Everything, because runCurrent() also advances virtual time through delays
- Nothing, because runCurrent() needs advanceUntilIdle() before it runs
- Only the work at the current virtual time; delay(10_000) stays pending
- Only the work after the delay; runCurrent() skips the immediate part first
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.