Flutter Testing
Write and maintain Flutter tests at the right level: unit tests for business logic, widget tests for screens and components, integration tests for critical flows, and golden tests for visual regression.
Test Pyramid
[Integration Tests] — critical end-to-end flows; device/emulator required; CI on merge only
[Widget Tests] — screens, components, interactions; run every commit
[Unit Tests] — business logic, providers, cubits, utilities; run every commit
Target distribution: 70%+ unit | 20–25% widget | 5–10% integration.
Setup
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^1.0.0
bloc_test: ^9.1.0
network_image_mock: ^2.1.0
Unit Tests
What to test: use cases, repository impls, Riverpod notifiers, Cubit/Bloc, validators, parsers, mappers, utilities.
Do NOT test: *.g.dart / *.freezed.dart, simple value objects with no logic, framework widgets (Text, Container), layout-only build methods.
Structure:
void main() {
late MockProductRepository mockRepository;
late GetProducts useCase;
setUp(() {
mockRepository = MockProductRepository();
useCase = GetProducts(mockRepository);
});
group('GetProducts', () {
test('returns list on success', () async {
when(() => mockRepository.getProducts()).thenAnswer((_) async => fakeProducts);
expect(await useCase.execute(), equals(fakeProducts));
});
test('propagates exception', () {
when(() => mockRepository.getProducts()).thenThrow(const NetworkException('No connection'));
expect(useCase.execute(), throwsA(isA<NetworkException>()));
});
});
}
Riverpod notifier tests: use ProviderContainer with overrideWithValue. Always call addTearDown(container.dispose).
Bloc/Cubit tests: use bloc_test blocTest<Cubit, State>() with build, act, expect, and optional verify.
See @references/state-management-snippets.md for full Riverpod and bloc_test examples.
Widget Tests
Minimum per screen: test all four states — loading, success, error, empty.
Also test: user interactions (tap, scroll, form input), navigation triggers, complex reusable components.
Riverpod widget test:
testWidgets('shows product list on success', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [productsNotifierProvider.overrideWith(() => FakeProductsNotifier())],
child: const MaterialApp(home: ProductsScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text(fakeProducts.first.name), findsOneWidget);
});
For loading state: use tester.pump() (one frame), NOT pumpAndSettle — the latter waits until animation completes and may wait forever.
Bloc widget test: wrap with BlocProvider<Cubit>.value(value: mockCubit) and use whenListen from bloc_test.
Finder priority (most to least stable):
find.byKey(const Key('widget-id'))— survives text changesfind.byType(MyWidget)— stable for design system componentsfind.text('Submit')— fragile if copy changes
Always add Key to dynamically generated list items:
ListView.builder(
itemBuilder: (context, i) => ProductTile(key: ValueKey(products[i].id), product: products[i]),
)
Integration Tests
// integration_test/app_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('sign in and navigate to home', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email-field')), 'test@example.com');
await tester.tap(find.byKey(const Key('sign-in-button')));
await tester.pumpAndSettle(const Duration(seconds: 5));
expect(find.byType(HomeScreen), findsOneWidget);
});
}
Rules:
- Use a dedicated test environment (staging Firebase / mock backend).
- Keep count small — integration tests are 10–100x slower than widget tests.
- Test only critical paths that widget tests cannot cover.
- Run in CI on merge to main only.
Golden Tests
Catch unintended visual regressions in design system components and critical screens.
testWidgets('PrimaryButton matches golden — default', (tester) async {
await tester.pumpWidget(/* MaterialApp > Scaffold > Center > PrimaryButton */);
await expectLater(find.byType(PrimaryButton), matchesGoldenFile('goldens/primary_button_default.png'));
});
Update after intentional design changes: flutter test --update-goldens
Test at multiple device sizes by setting tester.view.physicalSize before pumping. Cover: all design system components, critical screens at phone/tablet breakpoints, dark vs. light mode.
CI Commands
flutter test # unit + widget
flutter test --coverage # with coverage
genhtml coverage/lcov.info -o coverage/html
flutter test integration_test/ -d <device>
flutter test --update-goldens
flutter test --name "pattern" # filter by test name
Coverage targets:
- Domain (use cases, entities): 90%+
- Data (repositories, datasources): 80%+
- Presentation (notifiers, cubits): 80%+
- UI widgets: covered by widget tests, not line coverage
Cross-references
- Code examples:
@references/state-management-snippets.md(Riverpod + Bloc test patterns) - Agent:
flutter-test-engineer - Error types:
@references/dart-error-mapping.md