skip to content

A Dart date library's retry-with-timeout and 'in 5 minutes' parsing tests wait real seconds and flake; how does fake_async make them fast and deterministic?

level: seniorimportance: should knowfreq 22%

answer

  1. a zone that fakes timers
  2. elapse moves fake time forward
  3. flushMicrotasks runs the express lane
  4. clock.now() instead of DateTime.now()
  5. a synchronous callback, no await inside

basics

~20 s

fakeAsync runs the test in a zone whose timers and microtasks fire only when you call elapse or flushMicrotasks, so a 30-second timeout takes no real time; reading time through package:clock's clock.now() lets it fake the current time too.

solid answer

~40 s

`fakeAsync((async) { ... })` from `package:fake_async` runs its callback in a `Zone` that intercepts `Timer` creation and microtasks. Nothing time-based fires on its own: `async.elapse(Duration(seconds: 30))` advances fake time and fires every timer due in that window, processing microtasks before and after each; `flushMicrotasks()` drains microtasks without moving time; `flushTimers()` elapses until no timers remain. So a retry loop with `Future.delayed` back-off or `Future.timeout` runs instantly and in the same order every time. `DateTime.now()` and `Stopwatch` are not controlled; code that reads time through `package:clock`'s `clock.now()` sees fake time, and `fakeAsync(..., initialTime: DateTime(2026, 9, 29))` pins the start. Keep the callback synchronous: an `async` callback that `await`s a timer-driven future never resumes, because nothing calls `elapse`.

code

dart · 19 lines
dart
import 'dart:async';

import 'package:clock/clock.dart';
import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';

DateTime inMinutes(int m) => clock.now().add(Duration(minutes: m));

void main() {
  test('timeout fires without waiting', () {
    fakeAsync((async) {
      final never = Completer<DateTime>().future;
      expect(never.timeout(const Duration(seconds: 10)),
          throwsA(isA<TimeoutException>()));
      async.elapse(const Duration(seconds: 10));
      expect(inMinutes(5), DateTime(2026, 9, 29, 9, 5, 10));
    }, initialTime: DateTime(2026, 9, 29, 9));
  });
}

go deeper

for a junior

Recall that fake_async lets a test skip real waiting by moving a fake clock forward with elapse.

for a middle

Explain the fake zone: timers and microtasks fire only on elapse or flushMicrotasks, and clock.now() follows the fake time.

for a senior

Show you design time-dependent code for testability through package:clock and injectable timers, and diagnose hangs from async callbacks inside fakeAsync.

for a principal

Set a codebase rule that time is always read through an injectable clock, trading a small indirection for deterministic tests everywhere.

## The flaky test A date library has two time-dependent features: - `parseRelative('in 5 minutes')` returns `DateTime.now().add(...)`, so the expected value changes every run and tests assert "roughly now"; - `fetchHolidays()` retries a remote source with back-off of 1, 2 and 4 seconds and gives up after `Future.timeout(Duration(seconds: 10))`. Tests for the second one take seven real seconds per failure path, and tests for the first fail when the clock ticks over a minute boundary between computing the expectation and the result. Both problems come from **real time**. ## What fake_async does `package:fake_async` (maintained with `package:test`) provides `FakeAsync` and a helper, `fakeAsync(callback, {initialTime})`. The callback runs inside a **`Zone`** that replaces timer and microtask scheduling. Inside it: - `Timer`, `Future.delayed`, `Future.timeout`, `Stream.periodic` and anything built on them do **not** fire by themselves; - you move time forward explicitly, and everything due in that window fires in order. | Method | Effect | |---|---| | `elapse(duration)` | advance fake time; fire due timers; microtasks processed before and after each timer | | `flushMicrotasks()` | run pending microtasks until none remain; timers untouched | | `flushTimers()` | keep elapsing until no timers are left (periodic ones included by default, with a one-hour safety timeout) | | `elapseBlocking(duration)` | simulate a slow synchronous call; nothing fires during it | | `pendingTimers`, `microtaskCount` | inspect what is still scheduled | ## Testing the retry path ```dart test('gives up after the 10 s timeout', () { fakeAsync((async) { final source = AlwaysFailingSource(); expect(fetchHolidays(source), throwsA(isA<TimeoutException>())); async.elapse(const Duration(seconds: 10)); expect(source.attempts, 4); // first try plus retries at 1, 3 and 7 s }); }); ``` The asynchronous matcher is registered first, then `elapse` fires the back-off timers and the timeout; the whole test takes milliseconds and cannot race. The attempt count follows from the back-off schedule in the example, not from fake_async itself. ## Controlling "now" `FakeAsync` cannot change what `DateTime.now()` or `Stopwatch` report; they are not part of `dart:async`. The package integrates with **`package:clock`**: inside `fakeAsync`, `clock.now()` and `clock.stopwatch()` report fake time. So the library should read time through `clock.now()`, and the test pins it: ```dart test('in 5 minutes is relative to the fake clock', () { fakeAsync((async) { expect(parseRelative('in 5 minutes'), DateTime(2026, 9, 29, 9, 5)); async.elapse(const Duration(hours: 1)); expect(parseRelative('in 5 minutes'), DateTime(2026, 9, 29, 10, 5)); }, initialTime: DateTime(2026, 9, 29, 9)); }); ``` ## Rules that keep it working 1. **Keep the callback synchronous.** An `async` callback that `await`s a future which needs a timer to fire never resumes, because the code that would call `elapse` comes after the `await`. If the test returns that `Future`, it hangs until the runner's timeout; if it drops it, the test ends early and the assertions after the `await` silently never run. 2. **Create the code under test inside the callback.** Timers scheduled before `fakeAsync` starts, or on real I/O, are not faked. 3. **Real I/O is not faked.** Sockets, files and isolates complete on their own schedule; fake them at a higher level. 4. **Drain at the end.** Check `pendingTimers` or call `flushTimers()` so a forgotten periodic timer does not hide a leak. 5. **Flutter's `testWidgets`** already runs in a fake-async zone of its own and advances time with `tester.pump`; that belongs with the Flutter widget-testing tools. ## Designing code so it can be faked fake_async works best when the code under test cooperates: - **Read time from `clock.now()`**, never `DateTime.now()`, in library code. A single rule, enforced in review, makes every time-dependent function testable. - **Schedule with `dart:async` primitives** (`Timer`, `Future.delayed`, `Future.timeout`, `Stream.periodic`) rather than polling loops on real I/O. - **Pass durations in**, such as back-off schedules and timeouts, so tests can reason about exact moments. - **Keep real I/O at the edges**, behind an interface the test replaces, so the faked part is pure timing logic. ## When not to use it For plain `async` code with no timers, such as parsing a file already in memory, `fake_async` adds nothing; an `async` test body with `await` is simpler. Reach for it when the behaviour under test is **about time**: delays, timeouts, retries, debouncing, expiry and relative dates.

  • Why does a fakeAsync test that awaits Future.delayed inside an async callback misbehave?
    Inside the fake zone, the delayed timer fires only when `elapse` or `flushTimers` is called. An `async` callback that awaits it suspends before reaching any such call, so the future never completes. If the test returns the callback's Future it hangs until the runner's timeout; if not, it finishes early and skips every assertion after the await.
  • Your library calls DateTime.now(); why does fakeAsync not control it, and what do you change?
    `DateTime.now()` reads the system clock directly and is outside `dart:async`, which is all fake_async replaces. Read time through `clock.now()` from `package:clock` instead; fakeAsync overrides that clock, and `initialTime` pins its starting value.

fake_async is a film editor's timeline: nothing plays until you drag the playhead, and dragging it ten seconds plays every scheduled scene in that span instantly and in order.

saying these in an interview costs you the question

  • Believes fakeAsync automatically fakes DateTime.now()
  • Writes the fakeAsync callback as async and awaits timer-driven futures
  • Thinks fake_async speeds up real network or file I/O
  • Fixes flaky timing tests by adding longer real delays
  • Expects timers to fire inside fakeAsync without calling elapse