skip to content

Advancing Virtual Time

advanceUntilIdle, advanceTimeBy, and runCurrent drive the virtual clock, and currentTime lets you assert on elapsed virtual time. This is how you test a debounce or retry-with-backoff in milliseconds of real time.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What is virtual time in coroutine tests, and why does a delay(10_000) inside runTest finish almost instantly instead of taking 10 seconds?

level: juniorimportance: must knowfreq 70%

answer

  1. TestCoroutineScheduler holds a fake clock
  2. delay registers an event, doesn't sleep
  3. runTest auto-advances when idle
  4. currentTime starts at 0, in ms
  5. fast + deterministic timing tests

basics

~10 s

Coroutine tests use a fake clock. Instead of really waiting, the test scheduler just moves the clock forward, so delays complete immediately and tests stay fast and deterministic.

solid answer

~40 s

runTest runs on a TestScope backed by a TestCoroutineScheduler that keeps a virtual clock. When a coroutine calls delay(10_000), it does not really sleep; the delay registers an event scheduled at virtual time +10_000 ms. runTest automatically advances the virtual clock (it auto-advances when the test body and scheduled tasks become idle) so the delay resumes without real waiting. This makes timing-dependent code (timeouts, debounce, retry backoff) testable in milliseconds and deterministic. You read the clock with the scheduler's currentTime (or testScheduler.currentTime) and can drive it manually with advanceTimeBy, advanceUntilIdle, and runCurrent. The clock starts at 0 and only moves when delays elapse or you advance it explicitly.

code

kotlin · 10 lines
kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlin.test.assertEquals

@Test
fun virtualTimeSkipsDelays() = runTest {
    assertEquals(0, currentTime)
    delay(10_000)              // resolves instantly via virtual time
    assertEquals(10_000, currentTime)
}

go deeper

for a junior

Knows tests don't really wait and delays finish fast; can name runTest.

for a middle

Explains the scheduler/virtual clock, currentTime, and auto-advance precisely.

for a senior

Connects virtual time to testing timeouts/debounce/backoff and notes Thread.sleep is NOT virtualized.

for a principal

Discusses determinism guarantees, when auto-advance vs manual control is appropriate, and library design tradeoffs.

## What virtual time is Real coroutine code uses `delay(ms)` to suspend without blocking a thread. In tests, really sleeping would make suites slow and flaky. The coroutines-test library replaces the real clock with a **virtual clock** managed by a `TestCoroutineScheduler`. - `runTest { ... }` is the entry point. It creates a `TestScope` whose dispatcher (`StandardTestDispatcher` by default) shares a single `TestCoroutineScheduler`. - The scheduler holds a `currentTime` (a `Long`, milliseconds, starting at `0`) and a queue of tasks each tagged with the virtual time at which they should run. - When code calls `delay(10_000)`, the coroutine suspends and registers a resume event at virtual time `currentTime + 10_000`. **No real time passes.** ## Why delay finishes instantly `runTest` **auto-advances** virtual time. When the test body and all currently-runnable tasks are idle (everyone is parked on a `delay`), the scheduler jumps `currentTime` forward to the next scheduled event and resumes it. So a `delay(10_000)` is skipped over in real terms — the whole test may complete in well under a second of wall-clock time. ```kotlin @Test fun delaysAreVirtual() = runTest { val start = currentTime // 0 delay(10_000) println(currentTime) // 10000, but ran instantly assertEquals(10_000, currentTime - start) } ``` ## Key APIs to know - `currentTime` — reads the virtual clock (on `TestScope` it delegates to `testScheduler.currentTime`). - `advanceTimeBy(ms)` — moves the clock forward by `ms`, running anything scheduled in that window (but **not** tasks scheduled exactly at the new time — see follow-up). - `advanceUntilIdle()` — runs everything until no scheduled tasks remain, moving the clock as needed. - `runCurrent()` — runs tasks scheduled at the **current** virtual time only, without advancing the clock. ## Why it matters Virtual time lets you assert on **timeouts** (`withTimeout`), **debounce/throttle**, **retry backoff**, and periodic emissions deterministically and fast. A test that would otherwise take minutes runs in milliseconds, and there is no race between real wall-clock time and assertions.

  • Does runTest auto-advancing mean you never need advanceTimeBy or runCurrent?
    No. Auto-advance fires when the body becomes idle. To assert intermediate state at a specific point in time (e.g., after 4s of a 5s debounce), you launch a coroutine and drive the clock manually with advanceTimeBy/runCurrent.
  • What unit is currentTime in?
    Milliseconds of virtual time, as a Long, starting at 0.

Like a film director yelling 'fast-forward' — the actors skip the 10-second pause instantly instead of standing around waiting.

saying these in an interview costs you the question

  • Thinks delay actually sleeps the test thread
  • Believes Thread.sleep behaves like delay under virtual time
  • Cannot name TestCoroutineScheduler or currentTime
  • Claims virtual time requires real wall-clock waiting
  • Confuses virtual time with a mock of the system clock (System.currentTimeMillis)

context

open as a page

Explain the difference between advanceUntilIdle(), advanceTimeBy(ms), and runCurrent(). When would you reach for each?

level: middleimportance: must knowfreq 75%

basics

~10 s

advanceUntilIdle 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.

open as a page

How do you use the scheduler's currentTime to assert timing-dependent behavior, such as verifying that a retry uses exponential backoff?

level: middleimportance: should knowfreq 55%

basics

~10 s

currentTime is the virtual clock in milliseconds. Record it before and after operations and check the difference to prove how long something waited — for example that each retry waited longer than the last.

open as a page

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%

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.

open as a page

You have a search box that debounces input by 300 ms before querying. Write/describe a test using virtual time that proves a query fires once after the window and not earlier.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Emit input, advance the virtual clock to just under 300 ms and check nothing queried yet, then advance past 300 ms and check exactly one query fired. Rapid input before the window should collapse into a single query.

open as a page