skip to content

In a Flutter widget test, what is the difference between tester.pump() and tester.pumpAndSettle(), and when is each the right call?

level: middleimportance: must knowfreq 66%

answer

  1. one frame vs a loop of frames
  2. pump(duration) advances the fake clock
  3. 100 ms steps until nothing is scheduled
  4. ten-minute timeout on the fake clock
  5. returns the number of pumps

basics

~20 s

tester.pump() renders one frame, optionally after advancing fake time by a duration; tester.pumpAndSettle() keeps pumping in 100 ms steps until no frame is scheduled. Use pump for precise control and pumpAndSettle to finish finite animations.

solid answer

~40 s

`pump([duration])` advances the test's fake clock by `duration` (if given), flushes microtasks and, if a frame is scheduled, runs one build-layout-paint pass. `pumpAndSettle([duration = 100 ms])` calls `pump(duration)` at least once and repeats while `hasScheduledFrame` is true, so it effectively waits for every running animation to finish; it returns the pump count and throws `pumpAndSettle timed out` once the fake clock passes its ten-minute `timeout`. I use `pump()` after a tap that just calls `setState`, `pump(const Duration(milliseconds: 150))` to check an animation mid-flight or fire a timer, and `pumpAndSettle()` after a route push or a dialog opening. It does not wait for timers that schedule no frame, and it never settles while an endless animation runs.

code

dart · 14 lines
dart
testWidgets('forgot-password dialog opens and the hint fades in', (WidgetTester tester) async {
  await tester.pumpWidget(const MaterialApp(home: Scaffold(body: LoginForm())));

  await tester.tap(find.byKey(const Key('forgot-password')));
  final int frames = await tester.pumpAndSettle();
  expect(frames, greaterThan(1));
  expect(find.byType(AlertDialog), findsOneWidget);

  await tester.enterText(find.byKey(const Key('login-email')), 'ana');
  await tester.pump();
  await tester.pump(const Duration(milliseconds: 100));
  final FadeTransition hint = tester.widget(find.byKey(const Key('email-hint-fade')));
  expect(hint.opacity.value, inExclusiveRange(0.0, 1.0));
});

go deeper

for a junior

Recall that pump renders one frame and pumpAndSettle keeps pumping until no frame is scheduled, and that frames never happen in a test unless you ask.

for a middle

Explain fake time: pump(duration) moves the fake clock and renders once, pumpAndSettle loops in 100 ms steps, returns a count and times out after ten fake minutes.

for a senior

Show when pumpAndSettle hides bugs or cannot finish - endless animations, timers that schedule no frame - and prefer explicit pumps that pin the expected frame count.

for a principal

Set a team convention: explicit pumps as the default, pumpAndSettle for transitions, and a review rule against raised timeouts that mask never-settling screens.

## Frames only happen when the test asks In a running app the engine asks for a new frame on every vsync while anything is dirty or animating. In a widget test run by `flutter test`, the binding is `AutomatedTestWidgetsFlutterBinding`: there is no vsync, and time is **fake** - it comes from a `FakeAsync` zone that only moves when the test moves it. That makes tests fast and deterministic, and it is why you drive the UI with explicit pumps. ## `pump([Duration? duration])` `tester.pump` does, in order: 1. If a `duration` is passed, **elapse** that much fake time. Any `Timer` or `Future.delayed` due within that window fires, and animation tickers will see the new timestamp. 2. **Flush microtasks** (so completed futures run their continuations). 3. If a frame is scheduled (`hasScheduledFrame`), run **one** frame: `handleBeginFrame` (animation ticks) then `handleDrawFrame` (build, layout, paint, semantics). So `pump()` with no argument means "render whatever is dirty now, without moving time", and `pump(const Duration(milliseconds: 300))` means "pretend 300 ms passed, then render one frame". Advancing time does not render intermediate frames - an animation jumps straight to the 300 ms value. ## `pumpAndSettle([Duration duration = 100 ms, ...])` `WidgetTester.pumpAndSettle` is a loop around `pump`: - It calls `pump(duration)` **at least once**, even if nothing is scheduled, to flush microtasks that might themselves schedule a frame. - It repeats while `binding.hasScheduledFrame` is true - that is, while any animation or dirty element keeps requesting frames. - It returns the **number of pumps** performed, which a test can assert. - Its `timeout` defaults to **ten minutes of fake time**. When the fake clock passes that, it throws `FlutterError('pumpAndSettle timed out')`. With the 100 ms default step that is around six thousand frames, which is why a doomed call takes seconds of wall time and looks like a hang before it fails. ## Side by side | | `pump()` | `pumpAndSettle()` | |---|---|---| | Frames per call | at most one | as many as needed until idle | | Fake time moved | exactly `duration` (zero by default) | `duration` per iteration, 100 ms by default | | Endless animation | fine - you choose how far | throws `pumpAndSettle timed out` | | Timer with no frame | fires only if `duration` covers it | may not fire: the loop stops once no frame is scheduled | | Returns | nothing | the pump count | ## Choosing between them - **After a plain `setState`** (a checkbox toggles, a validation message appears): `pump()` is enough and states exactly what you expect. - **After navigation or a dialog** (`Navigator.push`, `showDialog`, a `SnackBar` sliding in): `pumpAndSettle()` runs the transition to completion. Mid-transition both pages are built and on stage, so a finder can match widgets on either; once it settles, the page under an opaque route goes offstage and finders skip it by default. - **Mid-animation checks**: `pump()` to start, then `pump(const Duration(milliseconds: 150))` and assert on the intermediate value. `pumpAndSettle` would only show you the end state. - **Debounces and delays**: a search field that fires after a 500 ms `Timer` needs `pump(const Duration(milliseconds: 500))`. `pumpAndSettle()` may return after one 100 ms step because the timer schedules no frame, leaving the timer pending. The framework's own documentation recommends working out exactly which frames a behaviour needs and pumping exactly those, because an explicit count catches regressions such as an animation starting one frame late. `pumpAndSettle` is a convenience, not a default. ## Common mistakes - Treating `pumpAndSettle` as "wait for all async work". It waits for **frames**, not for futures, timers or I/O. - Calling `pumpAndSettle` with a screen that shows an indeterminate `CircularProgressIndicator`; it can never settle. - Believing `pump(duration)` renders every frame in between; it renders one. - Passing a large custom `timeout` to hide a never-ending animation instead of fixing what keeps scheduling frames.

  • Does pump(const Duration(seconds: 1)) render a frame for every 16 ms in that second?
    No. It elapses one second of fake time and then runs at most one frame. Animations jump to their one-second value; any intermediate states are never built. To observe intermediate frames, call `pump` repeatedly with smaller durations, or use `pumpFrames` with a target widget and a frame interval.
  • Why can pumpAndSettle return while a Future.delayed of two seconds is still pending?
    It loops only while a frame is scheduled. A pending `Timer` schedules no frame, so after the first 100 ms pump the loop ends. The timer stays pending and the test later fails with 'A Timer is still pending even after the widget tree was disposed' unless you `pump(const Duration(seconds: 2))`.
  • Why assert the return value of pumpAndSettle?
    It is the number of pumps it took to settle. Asserting it - or pumping an exact number of frames instead - catches regressions where an animation got longer, started a frame late, or began looping.

pump is taking one photo after winding a clock forward by a set amount; pumpAndSettle keeps taking a photo every tenth of a second until the scene stops moving, and gives up if the scene never stops.

saying these in an interview costs you the question

  • pumpAndSettle waits for every pending Future and timer to complete
  • pump(duration) renders every intermediate frame across that duration
  • pumpAndSettle is always the safer choice after any interaction
  • Raising the pumpAndSettle timeout is the fix for a screen that never settles
  • Widget test time is real time, so a 500 ms debounce needs a real sleep