skip to content

With a StandardTestDispatcher, a coroutine you launch inside runTest doesn't run immediately. How do runCurrent() and advanceUntilIdle() relate to making it execute?

level: seniorimportance: should knowfreq 50%

answer

  1. Standard = lazy: launch is queued, not run
  2. runCurrent flushes now-due tasks
  3. advanceUntilIdle drains everything
  4. runTest auto-drains when the body ends
  5. Unconfined = eager, runs to first suspension immediately

basics

~10 s

StandardTestDispatcher queues launched coroutines instead of running them right away. runCurrent() runs what's queued now; advanceUntilIdle() runs everything to the end. Without one of them, the launched code may never execute before your assertions.

solid answer

~40 s

runTest defaults to StandardTestDispatcher, which is 'lazy': a child launch/async is enqueued on the scheduler at the current virtual time but does NOT start until the test yields control. runCurrent() executes those now-due tasks (running each child up to its first suspension), and advanceUntilIdle() drains everything including delays. So after launch { ... }, you typically call runCurrent() to flush immediate work, or advance the clock to let delays elapse. This explicit stepping is what makes Standard deterministic and order-controlled. Contrast UnconfinedTestDispatcher, which starts coroutines eagerly until their first suspension — there a launched body runs immediately without runCurrent, but you lose fine-grained ordering control. Knowing this prevents 'my assertion ran before the coroutine did' failures.

code

kotlin · 15 lines
kotlin
@Test
fun standardVsUnconfined() = runTest {              // StandardTestDispatcher
    var hits = 0
    launch { hits++ }
    assertEquals(0, hits)        // queued, not run
    runCurrent()
    assertEquals(1, hits)        // now flushed
}

@Test
fun eager() = runTest(UnconfinedTestDispatcher()) {
    var hits = 0
    launch { hits++ }            // runs immediately to first suspension
    assertEquals(1, hits)        // already 1, no runCurrent needed
}

go deeper

for a junior

Knows you sometimes must call runCurrent/advanceUntilIdle for launched code to run.

for a middle

Explains Standard is lazy/queued and picks runCurrent vs advanceUntilIdle correctly.

for a senior

Articulates the Standard-vs-Unconfined eager/lazy contrast and why Standard is the deterministic default.

for a principal

Weighs determinism vs convenience across a test suite and sets dispatcher conventions to avoid order-hiding bugs.

## Why the launched coroutine 'doesn't run' `runTest` uses a **StandardTestDispatcher** by default. Its scheduling policy is **lazy/fair**: when you `launch { ... }` (or `async`), the new coroutine is **queued** on the `TestCoroutineScheduler` at the current virtual time rather than executed inline. The parent (your test body) keeps running until *it* suspends or finishes. So immediately after `launch`, the child hasn't started — assertions about its side effects would fail. ```kotlin @Test fun mustFlush() = runTest { var ran = false launch { ran = true } // queued, not executed yet assertFalse(ran) // still false! runCurrent() // now the queued task runs assertTrue(ran) } ``` ## runCurrent() vs advanceUntilIdle() here - **runCurrent()** runs all tasks due at the **current** virtual time — it flushes the just-launched coroutine up to its first suspension point (e.g., first `delay`), without moving the clock past any delay. - **advanceUntilIdle()** runs everything to completion, advancing the clock through delays. Use it when you don't care about intermediate steps and just want the end state. ## Why runTest often 'just works' anyway When the **test body itself finishes or suspends**, `runTest` auto-advances and drains pending children before the test ends, so simple tests pass without explicit `runCurrent`. The explicit calls matter when you assert **mid-flight**, between launch and completion. ## Contrast: UnconfinedTestDispatcher `UnconfinedTestDispatcher` is **eager**: a `launch` runs its body immediately (on the calling thread) until the first suspension. There, `ran` would be `true` without `runCurrent()`. The tradeoff: Unconfined gives convenient eager execution but **less control over interleaving/ordering**, which can hide ordering bugs. Standard is the deterministic default for that reason. ## Practical rule - Need to assert something a child did *immediately*? `runCurrent()` after `launch`. - Need delays to elapse and everything settled? `advanceTimeBy(...)` or `advanceUntilIdle()`. - Just want the final result with no intermediate checks? Let `runTest` drain at the end, or call `advanceUntilIdle()` before final asserts.

  • If runTest auto-drains at the end, why ever call runCurrent() explicitly?
    To assert intermediate state between launch and completion. Auto-drain only happens once the body is idle/finished; mid-flight assertions need explicit stepping.
  • Why is StandardTestDispatcher the default rather than Unconfined?
    Standard's lazy, fair scheduling is deterministic and forces explicit control over execution order, which surfaces ordering/concurrency bugs. Unconfined's eager execution is convenient but can mask them.

Standard dispatcher is a deli ticket queue — you launch, you take a number, nothing happens until you call 'next' (runCurrent). Unconfined skips the line and serves you on the spot.

saying these in an interview costs you the question

  • Believes launch always runs eagerly under StandardTestDispatcher
  • Can't explain why an assertion sees stale state after launch
  • Thinks runCurrent and advanceUntilIdle are interchangeable in all cases
  • Reaches for Thread.sleep to 'let the coroutine run'
  • Doesn't know Unconfined runs eagerly to first suspension

context