skip to content

Async Output & Fake Time

The two clocks a component test advances — the framework's update queue and real or faked time — and asserting the pending output before the resolved one. Interleaving them wrong is the classic flake.

on this pageshow

questions

5

In a component test, what does replacing real time with a fake clock make testable that a real delay cannot?

level: juniorimportance: must knowfreq 58%

answer

  1. time is an injected dependency
  2. waiting becomes an explicit move
  3. delay windows reached without delay
  4. advance the clock, then settle
  5. restore it in teardown

basics

~20 s

A fake clock turns waiting into an explicit step: the test moves time forward itself, so a debounce window, a poll interval, a timeout branch or a retry delay is reached instantly and identically on every run.

solid answer

~50 s

Timers, repeating intervals and animation-frame callbacks come from the host, and a test that waits on them pays real wall-clock time and inherits real jitter. Installing a fake clock swaps those host entry points for a queue the test drives: advancing by a stated amount fires exactly the callbacks whose due time falls inside that window, in scheduled order, with nothing else moving. That makes time-shaped branches cheap and repeatable - the pause a debounced field waits for, the second tick of a poll, the branch a request takes when it passes its own deadline, the gap between retries, the label a relative timestamp renders. Two limits matter: advancing the clock only runs the due callbacks, so committed output still needs the framework's own settle step, and the clock is shared process state, so it has to be restored when the test ends.

go deeper

for a junior

Recall that a test can own the clock: instead of waiting out a delay, it moves time forward by a stated amount and the callbacks due in that window fire.

for a middle

Be able to name what a fake clock replaces - one-shot timers, intervals, frame and idle callbacks, the current-time reading - and to say that firing a callback is not the same as the framework rendering its result.

for a senior

Show the operational half: install and restore in shared hooks, advance in steps that match the product's real windows, and treat callbacks left pending at teardown as a defect rather than noise.

for a principal

Frame the tradeoff. A controlled clock buys determinism for delay-shaped logic and says nothing about the durations users actually feel, so decide deliberately where in the portfolio real timing is measured instead.

## Time is a dependency, not a background fact A component test already controls its inputs: the props it mounts with, the values its ancestors provide, the events it dispatches into bindings. Time is an input too, and it is the one teams habitually leave uncontrolled. A component that debounces a field, polls for a status, gives a request a deadline, spaces retries apart, or renders a relative timestamp has behaviour whose **trigger is the passage of time**. Left real, that behaviour costs wall-clock seconds to reach and arrives with jitter: a loaded machine can miss a window by enough to change which branch runs, which is how a correct component acquires an intermittently failing test. Installing a fake clock makes time an argument the test supplies. The host's time-driven entry points are replaced by a queue the test owns, and advancing that queue by a stated amount fires exactly the callbacks whose due time falls inside it, in scheduled order, while nothing else moves. ## What installing a clock takes over - **one-shot timers** - the primitive a debounce, a deadline and a retry delay are built on; - **repeating intervals** - the primitive a poll is built on; - **animation-frame callbacks** - frame-aligned work, and often a framework's own batching; - **idle callbacks** - low-priority work a framework or a component defers; - **the current-time reading** - what a component renders a relative timestamp, a countdown or an expiry from. Most clocks let a test choose which of these to take over. That choice matters, because the more entry points you freeze, the more of the framework's own machinery you may have frozen with them. ## What becomes testable | Behaviour | Under real time | Under a controlled clock | |---|---|---| | A debounce window | wait out the real pause in every test | move past the window and assert the single resulting action | | A poll interval | the second tick arrives seconds later | advance one interval at a time and count what was requested | | A deadline branch | reachable only by making the work genuinely slow | advance past the deadline and assert the timeout output | | A retry delay | the suite pays the sum of every backoff | advance each gap and assert the attempt sequence | | A relative timestamp | the rendered text changes between runs | pin the reading so the text is a constant | The pattern in that column is the point: a delay stops being something the test endures and becomes something the test **states**. A window of two hundred milliseconds and a backoff of thirty seconds cost the same - one advance each. ## What advancing the clock does not do - It does not drain the framework's pending update work. A timer callback writes state; turning that write into committed output is a separate settle step. - It does not release work that no timer gates. Continuation queues are drained on every yield and never consult the clock, so pending async work resolves on its own schedule. - It does not speed up code that scheduled nothing. Synchronous work that is simply slow is unaffected. - It does not decide what a pending request returns. A fake clock controls *when* something happens, never *what* it produces. - It does not change host-computed facts. Element geometry, style matching and focus order come from the host, and no clock position moves them. ## The discipline that keeps it honest 1. **Install per test, in shared setup.** A clock installed ad hoc in a test body is a clock that leaks when the body fails early. 2. **Advance in named steps that match real windows.** Advancing by the product's actual debounce or interval documents the behaviour; advancing by an arbitrary large jump hides which window mattered. 3. **Settle after advancing.** The two queues are separate, and asserting between them is the classic flake. 4. **Restore in teardown.** Shared host state that one test replaced must be given back whether the test passed or failed. ## Honest limits A clock-driven suite proves the *logic* of a delay, not the duration a user feels. It says a debounce fires once after its window, never that the window is well chosen; it says a deadline branch renders, never that the deadline is reachable on a slow connection. Those are judgments for a different layer of the portfolio. What the fake clock buys at this layer is the ability to test the branch at all - and to test it in milliseconds, the same way, on every machine that runs the suite.

  • What does a controlled clock not help with when a component waits on work that no timer gates?
    Nothing at all - that work is released by its own resolution, not by time. Advancing the clock fires no callback for it, so the test still has to let queued continuations run and then settle the framework before output appears. A clock only helps where a delay is genuinely the gate.
  • Why is a component's reading of the current time worth pinning as well as its timers?
    Because output derived from the current time - a relative timestamp, a countdown, an expiry badge - changes by itself between runs, so no stable assertion can be written against it. Pinning the reading makes that output a constant, and advancing the clock then lets the test check how it changes.
  • When is waiting real time still defensible in a component test?
    Rarely, and never for logic you can reach by moving the clock. It is defensible when the behaviour belongs to the host and is not exposed through an entry point the test can replace; even then the test should wait on a readiness condition rather than a fixed duration, so it is not encoding a guess about speed.

saying these in an interview costs you the question

  • Treats a fixed real sleep as equivalent to advancing a controlled clock
  • Thinks advancing the clock also drains the framework's pending update work
  • Expects a faked clock to release async work that no timer was gating
  • Leaves the clock installed after the test, so later tests inherit frozen time
  • Believes faking time speeds up synchronous code that scheduled nothing
open as a page

With time faked in a component test, why must the clock be advanced and the framework's update queue settled separately?

level: middleimportance: must knowfreq 56%

basics

~20 s

They are two different queues with two different triggers. Advancing the clock fires timer callbacks, and those callbacks only write state; turning a write into committed output is the framework's settle step. Advance, settle, then assert - once per link.

open as a page

In a component test, at which point in the advance-and-settle sequence do you assert a component's pending output?

level: middleimportance: should knowfreq 47%

basics

~20 s

Immediately after the settle that commits the write starting the async work, and before the clock advances or the work is released. The pending shape exists only in that window; an end-state assertion cannot prove it appeared.

open as a page

A component suite's later tests hang only when one earlier test runs first; how can a faked clock cause that?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The clock is process-wide state. A test that installs it and never restores it leaves later tests with time frozen, so their debounce, poll and timeout waits never fire - and its leftover callbacks can fire into a torn-down tree.

open as a page

Why can installing a fake clock stop a framework's update scheduling from advancing while a microtask-based flush keeps working?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Because frameworks batch commit work onto different host entry points. A flush drained on the continuation queue is not time-driven and keeps running; work parked on a timer, idle callback or animation frame advances only when the frozen clock moves.

open as a page