Explain the difference between advanceUntilIdle(), advanceTimeBy(ms), and runCurrent(). When would you reach for each?
answer
- runCurrent = now only, clock stays
- advanceTimeBy = move clock, window is exclusive at the end
- advanceUntilIdle = play to the end
- after advanceTimeBy, runCurrent flushes the boundary task
- currentTime tells you how far you moved
basics
~10 sadvanceUntilIdle runs all pending work to completion. advanceTimeBy moves the clock a fixed amount and runs what falls in that window. runCurrent runs only tasks already due now, without moving the clock.
solid answer
~40 sAll three live on TestCoroutineScheduler (exposed via TestScope). runCurrent() executes every task scheduled at the current virtual time but does not advance the clock — useful to flush a just-launched coroutine to its first suspension point. advanceTimeBy(ms) moves currentTime forward by ms, executing tasks whose scheduled time falls within (current, current+ms]; in current kotlinx-coroutines it does NOT run tasks scheduled exactly at the new currentTime — you call runCurrent() after to flush those. advanceUntilIdle() repeatedly runs and advances until no scheduled tasks remain, jumping the clock to whatever time is needed; it's the 'fast-forward to the end' tool. Reach for runCurrent to assert immediate effects, advanceTimeBy to test behavior at a precise instant (e.g., 4s into a 5s debounce), and advanceUntilIdle to drain everything before final assertions.
code
kotlin · 17 lines@Test
fun threeDrivers() = runTest {
val seen = mutableListOf<Int>()
launch {
delay(100); seen += 1
delay(100); seen += 2
}
runCurrent() // launches body, parks at delay(100)
assertEquals(emptyList<Int>(), seen)
advanceTimeBy(100); runCurrent() // boundary task at t=100 flushed
assertEquals(listOf(1), seen)
advanceUntilIdle() // run the rest
assertEquals(listOf(1, 2), seen)
assertEquals(200, currentTime)
}go deeper
Knows advanceUntilIdle drains pending work and is the common default.
Distinguishes all three accurately and picks the right one per scenario.
Knows the advanceTimeBy boundary nuance and the infinite-reschedule hang risk.
Reasons about deterministic test design, when manual stepping beats auto-advance, and how to structure tests of unbounded streams.
## The three drivers All are methods on `TestCoroutineScheduler`, surfaced on `TestScope` so you can call them directly inside `runTest`. ### runCurrent() Runs **all tasks scheduled at the current virtual time** and does **not** move the clock. A freshly `launch`ed coroutine is queued at the current time; `runCurrent()` lets it execute up to its first real suspension (e.g., its first `delay`). Use it to observe immediate side effects without skipping any delay. ### advanceTimeBy(delayTimeMillis) Moves `currentTime` forward by the given amount, executing every task whose scheduled virtual time lies in the half-open window `(oldTime, oldTime + ms)`. **Important nuance (current behavior):** a task scheduled *exactly* at the new `currentTime` is **not** run by `advanceTimeBy` — it's left pending. Call `runCurrent()` afterward to flush it. This is the precise tool for asserting state at a specific instant. ```kotlin val log = mutableListOf<String>() launch { delay(1000); log += "a" delay(1000); log += "b" } advanceTimeBy(1000) // window (0,1000), task at exactly 1000 NOT run yet assertEquals(emptyList<String>(), log) runCurrent() // now runs the task due at 1000 assertEquals(listOf("a"), log) ``` ### advanceUntilIdle() Runs and advances repeatedly until the scheduler has **no remaining scheduled tasks**, jumping `currentTime` forward as required. It's 'play to the end'. After it returns, everything that was going to happen has happened (unless a task reschedules forever, which would loop). ## Choosing one - **runCurrent()** — flush work due *now*; assert immediate effects; don't skip delays. - **advanceTimeBy(ms)** — assert behavior at a precise moment (mid-debounce, just-before vs just-after a timeout). - **advanceUntilIdle()** — drain the world before final assertions; the common default when you only care about the end state. ## Reading the clock Use `currentTime` (or `testScheduler.currentTime`) to verify how far virtual time moved — e.g., after `advanceUntilIdle()` it equals the time of the last completed task.
- After advanceTimeBy(1000), why might a coroutine's delay(1000) not have resumed?advanceTimeBy runs tasks in (old, old+ms) — a task scheduled exactly at the new currentTime sits at the boundary and isn't executed. Follow with runCurrent() (or advance a hair further) to flush it.
- Could advanceUntilIdle() hang?If a coroutine reschedules itself endlessly (e.g., an infinite loop of delay), there's never an idle state, so advanceUntilIdle would loop forever. Such code needs a different testing strategy (collect N items, then cancel).
saying these in an interview costs you the question
- Says advanceTimeBy runs the boundary task scheduled exactly at the new time
- Thinks runCurrent advances the clock
- Uses advanceUntilIdle when needing to assert mid-flight intermediate state
- Cannot explain that all three live on TestCoroutineScheduler
- Confuses advanceUntilIdle with simply yielding once