Given a StandardTestDispatcher, contrast runCurrent(), advanceTimeBy(n), and advanceUntilIdle() in terms of which queued coroutines they execute and how they move virtual time.
answer
- runCurrent: due-now only, no clock move
- advanceTimeBy(n): clock += n, boundary task excluded
- advanceUntilIdle: drain everything, clock jumps
- advanceTimeBy then runCurrent to include boundary
- currentTime reads the virtual clock
basics
~20 srunCurrent() runs tasks due at the current time without moving the clock. advanceTimeBy(n) moves the clock forward by n and runs everything due in that window. advanceUntilIdle() runs everything until the queue is empty, jumping time as needed.
solid answer
~50 sAll three drive the shared TestCoroutineScheduler that a StandardTestDispatcher queues onto. runCurrent() executes all tasks scheduled at the *current* virtual time (including new ones they enqueue at the same instant) but does not advance the clock, so delayed tasks stay pending. advanceTimeBy(n) advances virtual time by n milliseconds and runs every task whose due time falls strictly before current+n (the boundary task exactly at current+n is not run by advanceTimeBy in current kotlinx versions — it runs at the new 'current' only via a following runCurrent or further advance). advanceUntilIdle() repeatedly advances and runs until no tasks remain anywhere on the timeline, effectively fast-forwarding to quiescence. Because StandardTestDispatcher never runs launched coroutines eagerly, you must call one of these to make progress; runTest implicitly calls advanceUntilIdle() at the end of the block. These give fine-grained control to assert intermediate state at chosen virtual instants.
code
kotlin · 19 lines@Test
fun levers() = runTest {
val log = mutableListOf<String>()
launch { log += "now" }
launch { delay(10); log += "t10" }
launch { delay(20); log += "t20" }
runCurrent()
assertEquals(listOf("now"), log)
advanceTimeBy(10) // boundary task at t=10 not yet run
runCurrent() // now run it
assertEquals(listOf("now", "t10"), log)
assertEquals(10, currentTime)
advanceUntilIdle()
assertEquals(listOf("now", "t10", "t20"), log)
assertEquals(20, currentTime)
}go deeper
Knows advanceUntilIdle runs everything; vague on the others.
Correctly separates runCurrent (no clock move) from advanceTimeBy/advanceUntilIdle.
Nails the advanceTimeBy boundary semantics and pairs it with runCurrent for inclusive behavior.
Uses these levers to design tests that assert state at precise virtual instants and reasons about runTest's final drain and backgroundScope.
## They all drive the scheduler With `StandardTestDispatcher`, launched coroutines sit in the `TestCoroutineScheduler` queue. These three functions are the levers that *run* queued work and move the **virtual clock**. ## runCurrent() Runs every task whose due time is the **current** virtual time, including tasks those tasks schedule at the same instant (it drains the current instant). It does **not** advance the clock — anything scheduled for a later time stays pending. ```kotlin launch { log += "now" } // due at current time launch { delay(10); log += "later" } runCurrent() // log == ["now"]; "later" still pending ``` ## advanceTimeBy(n) Moves virtual time forward by `n`, running all tasks due **strictly before** the new time `current + n`. A task scheduled *exactly* at `current + n` is **not** executed by `advanceTimeBy` itself in current kotlinx-coroutines versions; it becomes due at the new current instant and runs on the next `runCurrent()`/advance. ```kotlin launch { delay(10); log += "t10" } advanceTimeBy(10) // runs tasks before t=10; the t=10 task is NOT yet run runCurrent() // now runs it ``` This boundary detail is a frequent source of off-by-one confusion; the safe pattern is `advanceTimeBy(n); runCurrent()` when you need the boundary task included. ## advanceUntilIdle() Keeps advancing the clock and running tasks until **nothing is left** scheduled. Time jumps to whatever the last task's due time is. Use it when you don't care about intermediate instants and just want everything to finish. ```kotlin launch { delay(1000); log += "done" } advanceUntilIdle() // jumps to t=1000, runs it // log == ["done"]; currentTime == 1000 ``` ## Reading the clock `testScheduler.currentTime` (and `TestScope.currentTime`) returns the current virtual time in ms — handy to assert how far time moved. ## How this interacts with runTest `runTest { ... }` runs the body on a `StandardTestDispatcher` by default and, after the body, calls `advanceUntilIdle()` to let queued work finish (anything left in `backgroundScope` is cancelled). So even if you forget to advance, the final drain happens — but only *after* your assertions, which is why intermediate assertions need explicit `runCurrent`/advance. ## Summary table - `runCurrent()` — run due-now tasks, clock unchanged. - `advanceTimeBy(n)` — clock += n, run tasks due before the new time (boundary excluded). - `advanceUntilIdle()` — run everything, clock jumps to last due time.
- Does advanceTimeBy(10) run a task scheduled exactly at t=10?No, not by itself in current kotlinx versions; it runs tasks due strictly before the new time. Follow with runCurrent() to include the boundary task.
- What does runTest do at the end of its block?It calls advanceUntilIdle() to let queued work finish, then cancels anything left in backgroundScope.
saying these in an interview costs you the question
- Thinking runCurrent advances the clock
- Assuming advanceTimeBy includes the exact-boundary task
- Confusing advanceUntilIdle (drain all) with runCurrent (now only)
- Believing you never need to advance under Standard
- Not knowing currentTime exposes the virtual clock