Testing Flutter apps

Widget tests that assert on behaviour, golden tests for visuals, integration tests on a real device, mocking with mocktail, and keeping coverage meaningful.

Widget tests

void main() {
  testWidgets('shows the empty state when there are no tasks', (tester) async {
    await tester.pumpWidget(
      MaterialApp(home: TaskListPage(repository: FakeRepository([]))),
    );
    await tester.pumpAndSettle();

    expect(find.text('No tasks yet'), findsOneWidget);
    expect(find.byType(CircularProgressIndicator), findsNothing);
  });

  testWidgets('adds a task and shows it in the list', (tester) async {
    final repo = FakeRepository([]);
    await tester.pumpWidget(MaterialApp(home: TaskListPage(repository: repo)));

    await tester.enterText(find.byKey(const Key('new-task-field')), 'Ship 1.2');
    await tester.tap(find.byKey(const Key('add-task-button')));
    await tester.pumpAndSettle();

    expect(find.text('Ship 1.2'), findsOneWidget);
    expect(repo.saved.single.title, 'Ship 1.2');
  });
}
  • Assert on what the user sees — text, semantics, keys — not on private widget fields.
  • pump advances one frame, pumpAndSettle runs until animations finish. An infinite animation makes pumpAndSettle time out.
  • Add Key values to elements a test must find; finding by position breaks the moment the layout changes.
  • Wrap network calls behind an injected repository so the widget test never opens a socket.

Golden and integration tests

testWidgets('metric card matches the golden', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      theme: lightTheme,
      home: const Scaffold(body: MetricCard(title: 'Open', value: '12')),
    ),
  );
  await expectLater(
    find.byType(MetricCard),
    matchesGoldenFile('goldens/metric_card.png'),
  );
});

// regenerate intentionally after a design change:
// flutter test --update-goldens
Test typeRuns onCost
UnitDart VMMilliseconds
WidgetFlutter test bindingTens of milliseconds
GoldenFlutter test bindingDepends on font rendering consistency
IntegrationDevice or emulatorMinutes

Golden files are sensitive to platform font rendering. Run them on a single pinned environment, usually a Linux container in CI, or you will chase differences that are not real changes.

Mocking and coverage

class MockApi extends Mock implements ApiClient {}

void main() {
  test('falls back to cached articles when the network fails', () async {
    final api = MockApi();
    when(() => api.articles()).thenThrow(ApiException(503, 'unavailable'));

    final repo = ArticleRepository(api: api, cache: InMemoryCache(['cached']));
    final result = await repo.load();

    expect(result, ['cached']);
    verify(() => api.articles()).called(1);
  });
}
⚠️
Coverage measures lines executed, not behaviour verified. A test that pumps a widget and asserts nothing raises coverage and catches nothing — review the assertions, not the percentage.

FAQ

How do I test a widget that uses <code>BuildContext</code> after an await?
Pump the widget inside a MaterialApp, trigger the action, then await tester.pumpAndSettle(). If the widget can be disposed mid-flight, dispose it in the test and assert that no exception is thrown.
Should integration tests run on every pull request?
Run a small smoke suite on every pull request and the full suite nightly. Full device suites are slow and flaky; a fast, reliable subset catches most regressions.

CI/CD, flavors and store deployment State management: setState, Provider, Riverpod and BLoC

Last refreshed 2026-09-18.