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.
pumpadvances one frame,pumpAndSettleruns until animations finish. An infinite animation makespumpAndSettletime out.- Add
Keyvalues 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 type | Runs on | Cost |
|---|---|---|
| Unit | Dart VM | Milliseconds |
| Widget | Flutter test binding | Tens of milliseconds |
| Golden | Flutter test binding | Depends on font rendering consistency |
| Integration | Device or emulator | Minutes |
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.
Related
CI/CD, flavors and store deployment State management: setState, Provider, Riverpod and BLoC
Last refreshed 2026-09-18.