How to Test Animations and Async UI Updates in Flutter: pump() vs pumpAndSettle()
Use pump() with a specific Duration to advance the animation clock by exact increments for testing intermediate states, and use pumpAndSettle() to automatically wait for all pending animations, timers, and asynchronous UI updates to complete.
The WidgetTester class drives the rendering pipeline in Flutter widget tests, offering precise control over frame advancement through two distinct APIs. When you need to test animations and async UI updates with pump vs pumpAndSettle, choosing the correct method depends on whether you require deterministic timing over individual frames or need to wait for the widget tree to reach a stable state. The flutter/skills repository demonstrates similar architectural patterns in its command-driven tooling, where synchronous steps and asynchronous coordination are cleanly separated across files like tool/generator/lib/src/commands/base_skill_command.dart and tool/dart_skills_lint/lib/src/validator.dart.
Understanding pump() and pumpAndSettle()
Deterministic Frame Control with pump()
The pump() method renders a new frame after a single tick of the event loop. You can optionally pass a Duration to simulate the passage of time, such as await tester.pump(const Duration(milliseconds: 100));. This method gives you explicit control over the animation clock, allowing you to stop at precise moments to verify interpolation values or UI states at specific time offsets.
Automatic Settlement with pumpAndSettle()
In contrast, pumpAndSettle() repeatedly calls pump() until the widget tree reports that no more frames are scheduled or a timeout (default 10 seconds) is reached. This method effectively "waits" for all pending animations, microtasks, and layout passes to complete, making it ideal for testing Future.delayed operations or animations driven by a TickerProvider where the exact duration is unknown.
When to Use Each Method
-
pump(Duration)– Use this when you want deterministic control over exact time steps. This is essential for stepping through an animation frame-by-frame to assert intermediate states (e.g., verifying opacity is exactly 0.3 at 300ms into a fade) or when testing code that schedules aFutureorTimerwith a known delay. -
pumpAndSettle()– Use this when you want to wait for all animations to finish without manually counting frames. This is the correct choice when the UI performs asynchronous work (e.g., mocked network fetches) and you only care about the final settled state, or when the exact duration of the animation is driven by internal configuration.
Code Examples
Testing Mid-Animation States with pump()
When you need to verify values during an animation rather than just the final state, use explicit durations with pump():
testWidgets('FadeTransition fades in over 1 second', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: FadeTransition(
opacity: const AlwaysStoppedAnimation(0.0),
child: const Text('Hello'),
),
),
);
// Verify initial opacity is 0.
final fadeFinder = find.text('Hello');
expect(tester.widget<FadeTransition>(find.byType(FadeTransition)).opacity.value, 0.0);
// Advance 300ms into the animation.
await tester.pump(const Duration(milliseconds: 300));
// Now opacity should be roughly 0.3 (linear curve by default).
expect(tester.widget<FadeTransition>(find.byType(FadeTransition)).opacity.value,
closeTo(0.3, 0.01));
});
Key points: pump(const Duration…) moves the animation clock forward exactly 300ms, letting you assert an intermediate state.
Waiting for Async Completion with pumpAndSettle()
When testing widgets that fetch data asynchronously, pumpAndSettle() handles the timing automatically:
testWidgets('Shows data after a mocked network delay', (WidgetTester tester) async {
// The widget fetches data in initState and shows a CircularProgressIndicator
// until the Future completes.
await tester.pumpWidget(const MyDataFetchingWidget());
// The spinner should be visible initially.
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Simulate the network delay (the widget internally uses Future.delayed).
await tester.pumpAndSettle(); // Waits for the Future and the subsequent rebuild.
// After settling, the spinner disappears and the data appears.
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('Loaded data'), findsOneWidget);
});
Key points: pumpAndSettle() automatically handles the Future.delayed and the UI update, without needing to know the exact delay.
Hybrid Approach for Complex Scenarios
For widgets with multiple animation phases, combine both methods for granular verification:
testWidgets('SnackBar disappears after its animation', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: MySnackBarButton())));
// Tap the button that shows a SnackBar.
await tester.tap(find.byType(ElevatedButton));
await tester.pump(); // Show the SnackBar.
expect(find.byType(SnackBar), findsOneWidget);
// The SnackBar slides in (animation) – advance half of the entrance duration.
await tester.pump(const Duration(milliseconds: 150));
// Verify the SnackBar is partially visible.
// (You could check its position if needed.)
// Now wait for the full lifecycle (entrance, display, exit).
await tester.pumpAndSettle();
// SnackBar should be gone.
expect(find.byType(SnackBar), findsNothing);
});
Key points: The test uses pump for a deterministic halfway-point check, then pumpAndSettle to let the rest of the animation and dismissal run automatically.
Common Pitfalls
Timeout Errors in Infinite Animations
If a widget repeatedly schedules frames (e.g., an infinite animation), pumpAndSettle() will time out after 10 seconds. In such cases, you must fall back to explicit pump() calls with a bounded duration rather than waiting for settlement.
Handling pumpAndSettle() Return Values
pumpAndSettle() returns a bool indicating completion status: it returns true if it timed out, false otherwise. Checking this return value can surface flaky tests early by distinguishing between incomplete animations and settled states.
Microtask Flushing Behavior
pump() advances the frame and flushes microtasks. If you only need to flush a Future without a visible frame change, a zero-duration await tester.pump(); is sufficient, as it processes microtasks without advancing the clock.
Architectural Parallels in flutter/skills
The separation between synchronous stepping and asynchronous waiting in Flutter tests mirrors the architecture found in the flutter/skills repository. The file tool/generator/lib/src/commands/base_skill_command.dart implements command execution as deterministic, isolated steps analogous to individual pump() calls. Similarly, tool/dart_skills_lint/lib/src/validator.dart processes validation logic in discrete steps, reflecting the frame-by-frame control that pump() provides.
For asynchronous coordination, tool/generator/lib/src/services/resource_fetcher_service.dart performs async fetches that require waiting for completion—paralleling how pumpAndSettle() waits for pending Future objects. The tool/dart_hooks/lib/src/dart_analyze_hook.dart file demonstrates awaiting process completion with timeout handling, similar to pumpAndSettle() repeatedly pumping until no more work remains. Finally, tool/dart_skills_lint/lib/src/rule_registry.dart registers lint rules in a manner analogous to how Flutter registers animation callbacks that pumpAndSettle() waits for before completing.
Summary
- Use
await tester.pump(Duration)when you need to test animations and async UI updates with pump vs pumpAndSettle by controlling exact time increments and verifying intermediate states. - Use
await tester.pumpAndSettle()to automatically wait for all animations, timers, and async operations to complete when only the final settled state matters. - Check the return value of
pumpAndSettle()(trueindicates timeout) to detect infinite animation loops and flaky tests early. - Reference architectural patterns in
tool/generator/lib/src/commands/base_skill_command.dartandtool/dart_hooks/lib/src/dart_analyze_hook.dartfrom the flutter/skills repository to understand the design philosophy separating synchronous steps from asynchronous coordination.
Frequently Asked Questions
Can I use pumpAndSettle() if my widget has an infinite loading animation?
No. pumpAndSettle() will throw a timeout error after 10 seconds because the animation continuously schedules new frames, never allowing the tree to settle. Instead, use pump(const Duration(milliseconds: 100)) with explicit bounds to test specific snapshots of the animation.
What is the default timeout for pumpAndSettle() and how do I change it?
The default timeout is 10 seconds. You can adjust this by passing a Duration to the timeout parameter, such as await tester.pumpAndSettle(const Duration(seconds: 5)).
How do I test a widget that uses Future.delayed without hardcoding the duration?
Use pumpAndSettle() without arguments. It automatically advances time until the Future completes and the widget rebuilds, regardless of whether the delay is 500 milliseconds or 5 seconds.
Does pump() run microtasks or just advance the clock?
pump() both advances the clock and flushes microtasks. A call to await tester.pump() with no duration argument specifically flushes microtasks without advancing time, which is useful for testing async logic that doesn't trigger visual updates but schedules Future resolution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →