Flutter has three test levels: unit tests (package:test) for plain Dart logic, widget tests (flutter_test: testWidgets, WidgetTester, find, matchers) that build real widgets in a fast headless environment, and integration tests (integration_test) that drive the whole app on a device. Write many of the first two and a few of the third.
- Unit tests use test(), group() and expect() from package:test to check a function or class in isolation; external services are replaced with fakes or mocks.
- Widget tests use testWidgets(), which hands you a WidgetTester: pumpWidget() builds a tree, find.* locates widgets, tester.tap/enterText/drag interact, and expect(finder, findsOneWidget) asserts.
- Nothing rebuilds on its own in a widget test: after an interaction call tester.pump() for one frame, pump(duration) to advance the fake clock, or pumpAndSettle() to run until no frames are scheduled.
- Widget tests run in a fake-async zone with a simulated screen, so they are nearly as fast as unit tests and do not need a device.
- Integration tests live in integration_test/, use the same flutter_test APIs, and run the real app on a device or emulator with `flutter test integration_test`; they cannot drive native OS dialogs.
- Test files end with _test.dart under test/; run them with `flutter test`, a single file with `flutter test test/x_test.dart`, or from the IDE for breakpoints.
| Unit | Widget | Integration | |
|---|---|---|---|
| What it exercises | One function, method or class | One widget (and its subtree) with layout and gestures | The whole app, or a large slice, end to end |
| Package | package:test (or flutter_test) | flutter_test | integration_test (+ flutter_test APIs) |
| Runs where | Dart VM on the host | Headless test binding on the host | Real device, emulator, desktop or browser |
| Speed | Milliseconds | Milliseconds to tens of ms | Seconds per test, plus app launch |
| Confidence | Low per test, high in volume | Higher: real build, layout, taps | Highest: real platform, plugins, navigation |
| Maintenance cost | Low | Higher: tied to UI structure | Highest: slow, flaky if not isolated |
| Key APIs | test, group, expect, setUp | testWidgets, WidgetTester, find, pump, pumpAndSettle, findsOneWidget | IntegrationTestWidgetsFlutterBinding, same tester APIs, flutter test integration_test |
The official guidance is a pyramid: many unit and widget tests, enough integration tests to cover the critical flows.
Bench test, wind tunnel, test flight
A unit test is bench-testing one part: you clamp the fuel pump to the workbench, feed it known inputs and measure the output. A widget test is a wind tunnel: the wing is real and the airflow is real physics, but the tunnel is a controlled room where you can freeze time between measurements (pump) and the plane never leaves the building. An integration test is a test flight: the actual aircraft on the actual runway, with everything connected. You bench-test every part, wind-tunnel the important assemblies, and fly a handful of carefully chosen routes.
Minimal working example
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_page.dart';
void main() {
testWidgets('tapping + increments the counter', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('Count: 0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
expect(find.text('Count: 0'), findsNothing);
});
}- L7testWidgets replaces test() and provides a fresh WidgetTester for this case.
- L8pumpWidget inflates the tree and renders one frame. Wrapping in MaterialApp supplies Directionality, theme and Navigator that most widgets need.
- L12tap() finds the widget's centre and sends a pointer down/up. It does not rebuild anything by itself.
- L13pump() runs one frame so the setState triggered by the tap is reflected in the tree.
- L15Finders are lazy queries; the matcher evaluates them against the current tree.
Use cases
- Pure logic (parsers, validators, view models, repositories): unit tests with package:test, mocking the network or database layer.
- A reusable widget or a whole screen's behaviour (what shows after a tap, what a form displays on invalid input): widget tests with testWidgets and finders.
- Critical user journeys (sign-in, checkout) and plugin-backed features: a few integration tests on real devices, often in CI.
- Visual regressions on custom-painted or heavily styled widgets: widget tests with matchesGoldenFile.
Benefits
- Widget tests give real layout, gestures and rebuild behaviour at unit-test speed, which is rare among UI frameworks.
- The same WidgetTester vocabulary works at the widget and integration levels, so tests can be promoted or demoted without rewriting.
- Dependency injection plus fakes makes async UI deterministic: no real timers, no network, no flakiness.
Limitations
- Widget tests use a simulated device (default 800x600 logical pixels, no platform channels), so plugin calls throw MissingPluginException unless mocked.
- pumpAndSettle cannot finish while an animation repeats forever; use pump(duration) and assert at specific moments instead.
- integration_test cannot interact with native UI such as permission dialogs; third-party packages (for example patrol) are needed for that.
- Golden files are platform-sensitive (fonts, anti-aliasing); keep them to one CI platform or they will drift.
Alternatives and trade-offs
- mockito (code-generated mocks) — You want when()/verify() stubbing with strict typing; add build_runner and annotate with @GenerateMocks.
- mocktail (no code generation) — You prefer to avoid build_runner; it offers a similar when()/verify() API using Mock subclasses and any() matchers. Not part of the Flutter SDK.
- Hand-written fakes — The dependency is a small interface; a fake class is clearer than a mock and has no extra tooling.
- Manual QA on devices — Checking platform-specific behaviour that no automated layer reaches; keep it for what tests cannot cover.
Asserting right after an interaction without pumping
Problem: tap(), enterText() and drag() change state, but the test binding does not render a new frame on its own. The expect runs against the old tree and fails even though the widget is correct.
Fix: Call await tester.pump() after each interaction (one frame), or pumpAndSettle() when animations or async work must complete first.
await tester.tap(find.byIcon(Icons.add));
expect(find.text('Count: 1'), findsOneWidget); // still shows 0await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);pumpAndSettle on a widget that never settles
Problem: A repeating AnimationController, an indeterminate progress indicator that stays on screen, or a periodic timer keeps scheduling frames, so pumpAndSettle loops until its timeout and throws.
Fix: Advance time explicitly with pump(const Duration(milliseconds: 300)) to the moment you want to assert, or stop the animation in the test (for example by injecting a flag that disables looping).
await tester.pumpWidget(const MaterialApp(home: CircularProgressIndicator()));
await tester.pumpAndSettle(); // times outawait tester.pumpWidget(const MaterialApp(home: CircularProgressIndicator()));
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(CircularProgressIndicator), findsOneWidget);Pumping a bare widget without MaterialApp or Directionality
Problem: Text, Scaffold and most Material widgets need a Directionality, MediaQuery or Theme ancestor. Without one the test fails with 'No Directionality widget found' or similar before your assertions run.
Fix: Wrap the widget under test in MaterialApp(home: ...) (or at least Directionality + MediaQuery) in a small helper used by every test in the file.
Hitting real services or real time in unit tests
Problem: A test that calls http.get or sleeps on real timers is slow, depends on the network, and fails for reasons unrelated to the code under test.
Fix: Accept the dependency through a constructor or parameter (an http.Client, a repository interface) and pass a fake or mock in the test; in widget tests rely on the fake-async clock instead of real delays.
This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.
- https://docs.flutter.dev/testing/overview
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/unit/introduction
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/unit/mocking
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/widget/introduction
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/widget/finders
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/widget/tap-drag
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/widget/scrolling
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/testing/integration/introduction
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- WidgetTester class API reference Flutter 3.47.7https://api.flutter.dev/flutter/flutter_test/WidgetTester-class.html
- Last checked
- Oct 21, 2026
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.