skip to content

How does Turbine's await timeout interact with kotlinx-coroutines-test virtual time, and how do you tune it?

level: seniorimportance: should knowfreq 35%

answer

  1. Turbine await default timeout = 3s, wall-clock
  2. runTest virtual time skips delay() in the flow
  3. Override via flow.test(timeout = X.seconds)
  4. Separate from runTest's own 60s test timeout
  5. Most timeouts = missing advanceTimeBy/runCurrent

basics

~20 s

Each await waits up to a timeout (3 seconds by default). Inside runTest, the flow's delays are skipped using virtual time, but a flow that truly never emits still hits the timeout and fails the test.

solid answer

~50 s

Turbine's await* functions enforce a timeout so a stuck test fails fast instead of hanging. The default is 3 seconds; you can override per call site via the timeout parameter on flow.test(timeout = ...) (a kotlin.time.Duration). Under runTest, the flow runs on a TestCoroutineScheduler with virtual time: delay() calls inside the flow are auto-advanced, so they don't consume the real timeout — Turbine's await effectively skips simulated delays. The timeout matters when production code blocks on something real (a never-emitting source, a deadlock, an un-advanced debounce). Turbine's timeout is wall-clock-based as a safety net even under virtual time, so very slow CI can still trip it; tune it up for genuinely slow setups, or fix the test to advance time. Don't confuse it with runTest's own dispatch timeout (the test body inactivity timeout), which is separate.

code

kotlin · 12 lines
kotlin
import kotlin.time.Duration.Companion.seconds

@Test
fun customTurbineTimeout() = runTest {
    flow {
        delay(2.seconds) // virtual: fast-forwarded
        emit(99)
    }.test(timeout = 5.seconds) {
        assertEquals(99, awaitItem())
        awaitComplete()
    }
}

go deeper

for a junior

Knows awaits have a timeout so tests don't hang forever.

for a middle

Knows the 3s default and that runTest skips delays, so real never-emitting sources cause timeouts.

for a senior

Distinguishes Turbine's wall-clock timeout from runTest's timeout, tunes via the Duration param, and diagnoses missing advanceTimeBy.

for a principal

Establishes guidance on timeout budgets and virtual-time discipline to keep a suite fast and non-flaky across CI environments.

## Two timeouts, one test There are **two distinct timeouts** in a Turbine-on-`runTest` test, and conflating them causes confusion: 1. **Turbine's await timeout** — how long any single `awaitItem()/awaitComplete()/awaitError()` waits before failing. **Default 3 seconds.** 2. **`runTest`'s coroutine-test timeout** — `runTest` fails if the whole test body makes no progress for its own timeout window (default 60s, configurable via `runTest(timeout = ...)`). ## Turbine timeout and virtual time Inside `runTest`, coroutines run on a `TestCoroutineScheduler` providing **virtual time**: a `delay(10_000)` in the flow is **fast-forwarded**, not actually waited. So a flow that emits after a long `delay()` still produces its item near-instantly in wall-clock terms, well within Turbine's 3s. Turbine's await timeout is a **wall-clock safety net**: it guards against a flow that genuinely never emits (e.g. awaiting an item from a source that's blocked, a missing `advanceTimeBy`, or a deadlock). In those cases there is no virtual-time event to advance to, so wall-clock elapses and the await fails with a timeout `AssertionError` — which is exactly what you want instead of a hung test. ## Tuning the Turbine timeout Pass `timeout` (a `kotlin.time.Duration`) to `test`: ```kotlin import kotlin.time.Duration.Companion.seconds @Test fun slowSource() = runTest { slowFlow.test(timeout = 10.seconds) { assertEquals(1, awaitItem()) cancelAndIgnoreRemainingEvents() } } ``` Use a larger value only when a **real** (non-virtual) wait is unavoidable, or for slow CI. Prefer advancing virtual time over inflating timeouts. ## The debounce / not-advanced trap If the flow uses `debounce`/`delay` and you call `awaitItem()` without advancing time, Turbine waits in wall-clock for an event that virtual time would have produced — and times out. Fix by advancing: ```kotlin input.debounce(100).test { input.emit(1) advanceTimeBy(101) runCurrent() assertEquals(1, awaitItem()) cancelAndIgnoreRemainingEvents() } ``` ## Key APIs to name - `flow.test(timeout: Duration?, name: String?) { }` - `runTest(timeout: Duration) { }` - `TestScope.advanceTimeBy(Duration)`, `runCurrent()`, `advanceUntilIdle()` - `kotlin.time.Duration` units like `3.seconds`. ## Summary Virtual time removes *simulated* delays; Turbine's timeout still protects against *real* stalls. Tune Turbine's `timeout` for slow real waits; tune `runTest`'s timeout for overall test budget. Most timeout failures are actually a missing `advanceTimeBy/runCurrent`, not a too-small timeout.

  • If a flow emits after delay(10.seconds), will Turbine's 3s timeout fail the test under runTest?
    No. Virtual time fast-forwards the delay, so the item appears almost instantly in wall-clock terms, well within 3 seconds.
  • Your awaitItem() times out on a debounced flow. What's the likely fix?
    You forgot to advance virtual time. Call advanceTimeBy(window) and runCurrent() so the debounce window elapses before awaiting.

saying these in an interview costs you the question

  • Claiming virtual time means awaits can never time out
  • Confusing Turbine's timeout with runTest's timeout
  • Inflating timeout instead of advancing virtual time
  • Thinking real delay()s are actually waited under runTest
  • Not knowing timeout is a Duration parameter on test

context