Skip to content
Boring Doc
Boring Doc explanation Verified against Flutter 3.47.7 3 min readtestingunit testwidget testintegration testflutter_test

Testing

Unit, widget and integration tests that stay fast.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
ComparisonUnit versus widget versus integration tests
UnitWidgetIntegration
What it exercisesOne function, method or classOne widget (and its subtree) with layout and gesturesThe whole app, or a large slice, end to end
Packagepackage:test (or flutter_test)flutter_testintegration_test (+ flutter_test APIs)
Runs whereDart VM on the hostHeadless test binding on the hostReal device, emulator, desktop or browser
SpeedMillisecondsMilliseconds to tens of msSeconds per test, plus app launch
ConfidenceLow per test, high in volumeHigher: real build, layout, tapsHighest: real platform, plugins, navigation
Maintenance costLowHigher: tied to UI structureHighest: slow, flaky if not isolated
Key APIstest, group, expect, setUptestWidgets, WidgetTester, find, pump, pumpAndSettle, findsOneWidgetIntegrationTestWidgetsFlutterBinding, 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.

Where the analogy breaks: In a wind tunnel time really passes; in a widget test it only advances when you pump, which is why forgetting pump() makes a correct widget look broken. And a test flight can only tell you the plane works, not why it failed; integration test failures still send you back to widget and unit tests to locate the fault.

Minimal working example

A widget test: build, tap, pump, assert
dart
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);
  });
}
Expected: The test passes in a few milliseconds without a device. Removing the pump() call makes the second expect fail because the tree still shows 'Count: 0'.Flutter ≥ 3.0.0An introduction to widget testing (cookbook)
  1. L7testWidgets replaces test() and provides a fresh WidgetTester for this case.
  2. L8pumpWidget inflates the tree and renders one frame. Wrapping in MaterialApp supplies Directionality, theme and Navigator that most widgets need.
  3. L12tap() finds the widget's centre and sends a pointer down/up. It does not rebuild anything by itself.
  4. L13pump() runs one frame so the setState triggered by the tap is reflected in the tree.
  5. 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.

✗ No frame between tap and assert
dart
await tester.tap(find.byIcon(Icons.add));
expect(find.text('Count: 1'), findsOneWidget); // still shows 0
✓ Pump a frame first
dart
await 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).

✗ Waits forever on a spinner
dart
await tester.pumpWidget(const MaterialApp(home: CircularProgressIndicator()));
await tester.pumpAndSettle(); // times out
✓ Advance a fixed amount
dart
await 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.

  • Testing Flutter apps Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/testing/overview
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • An introduction to unit testing Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/testing/unit/introduction
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Mock dependencies using Mockito Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/testing/unit/mocking
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • An introduction to widget testing Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/testing/widget/introduction
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Find widgets Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/testing/widget/finders
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Tap, drag, and enter text Flutter 3.47.7 Explanation verified against current source
    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
  • Handle scrolling Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/testing/widget/scrolling
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Integration testing concepts Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/cookbook/testing/integration/introduction
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • https://api.flutter.dev/flutter/flutter_test/WidgetTester-class.html
    Last checked
    Oct 21, 2026
Was this useful?
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set 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.