| name | flutter-testing |
| description | Use this skill whenever tests are being written, modified, or debugged in a Flutter project — including unit tests for providers/repositories, widget tests for screens, golden tests for visual regression, and integration tests for end-to-end flows. Triggers include "write a test for this", "test this Flutter code", "widget test", "golden test", "mock the API", "test a Riverpod provider", "pumpAndSettle", or any failing test discussion. Apply even when the user doesn't ask explicitly — when modifying production Flutter code, propose the matching tests. |
Flutter Testing
Encodes testing conventions for Flutter apps using Riverpod and an OpenAPI-generated client. Three layers, each with a clear job:
| Layer | What it tests | Speed | Where it runs |
|---|
| Unit | Pure Dart logic, repositories, notifiers | Fast | test/ via flutter test |
| Widget | Single screen or widget tree | Medium | test/ via flutter test |
| Golden | Visual regression of widgets | Medium | test/ via flutter test --update-goldens |
| Integration | Full app flows on a real device/emulator | Slow | integration_test/ via flutter test integration_test/ |
Folder layout — mirror lib/
test/
├── core/
│ └── api/
│ └── auth_interceptor_test.dart
├── features/
│ └── auth/
│ ├── data/
│ │ └── auth_repository_test.dart
│ └── presentation/
│ ├── auth_notifier_test.dart
│ └── login_screen_test.dart
├── helpers/
│ ├── pump_app.dart # Wraps widget in ProviderScope + MaterialApp
│ └── fakes.dart # Reusable fakes
└── goldens/
└── login_screen.png
A test file lives next to the source file's mirror path. lib/features/auth/data/auth_repository.dart → test/features/auth/data/auth_repository_test.dart. No exceptions.
Unit-testing Riverpod providers
Use ProviderContainer directly. Always tear down.
test('login success updates currentUser', () async {
final fakeRepo = FakeAuthRepository()..userToReturn = const User(id: '1');
final container = ProviderContainer(overrides: [
authRepositoryProvider.overrideWithValue(fakeRepo),
]);
addTearDown(container.dispose);
await container.read(authNotifierProvider.notifier).login('a@b.c', 'pw');
final user = container.read(currentUserProvider).value;
expect(user?.id, '1');
});
Rules:
- One
ProviderContainer per test, always with addTearDown(container.dispose).
- Override at the boundary (repositories, API client, storage) — not at the notifier level. Testing real notifier logic with fake repos catches the most bugs.
- Don't test framework behavior (don't assert that
ref.watch rebuilds — that's Riverpod's job).
Fakes over mocks
Prefer hand-written fakes to mockito/mocktail for repositories and services. They're explicit, refactor-cleanly, and read as documentation of how the real thing behaves.
class FakeAuthRepository implements AuthRepository {
User? userToReturn;
Failure? errorToThrow;
final calls = <String>[];
@override
Future<User> login(String email, String password) async {
calls.add('login:$email');
if (errorToThrow != null) throw errorToThrow!;
return userToReturn ?? (throw StateError('Fake not configured'));
}
}
Use mocktail only when stubbing a 3rd-party type you don't control (e.g. Dio itself).
Widget tests — the pumpApp helper
// test/helpers/pump_app.dart
Future<void> pumpApp(
WidgetTester tester, {
required Widget child,
List<Override> overrides = const [],
}) async {
await tester.pumpWidget(
ProviderScope(
overrides: overrides,
child: MaterialApp(home: child),
),
);
}
Every widget test uses it. This ensures consistent setup and lets you swap MaterialApp for a router-aware variant when needed.
testWidgets('login screen shows error on bad credentials', (tester) async {
await pumpApp(tester,
child: const LoginScreen(),
overrides: [
authRepositoryProvider.overrideWithValue(
FakeAuthRepository()..errorToThrow = const UnauthorizedFailure('bad creds'),
),
],
);
await tester.enterText(find.byKey(const Key('email')), 'a@b.c');
await tester.enterText(find.byKey(const Key('password')), 'wrong');
await tester.tap(find.byKey(const Key('submit')));
await tester.pumpAndSettle();
expect(find.text('bad creds'), findsOneWidget);
});
Use Keys on interactive elements you'll query in tests. Don't query by text alone — text changes during i18n updates and the test breaks for the wrong reason.
pump vs pumpAndSettle
pump() — one frame. Use when you want to assert an intermediate state (loading spinner visible).
pumpAndSettle() — runs until no frames are scheduled. Use after triggering an action that completes synchronously-ish.
- Avoid
pumpAndSettle with infinite animations (loading shimmer, marquee) — it hangs. Use pump(Duration(seconds: 1)) instead.
Golden tests
Use for design-system widgets (buttons, cards, form fields) and key screens in 2–3 representative states. Don't golden every screen — maintenance cost compounds.
testWidgets('PrimaryButton — default', (tester) async {
await pumpApp(tester, child: const PrimaryButton(label: 'Continue'));
await expectLater(
find.byType(PrimaryButton),
matchesGoldenFile('goldens/primary_button_default.png'),
);
});
Run on a single platform (Linux CI) only. Cross-platform golden diffs are a known pain — pin the runner.
Integration tests
Reserved for critical user journeys — login, primary booking/purchase flow, payment. Not for every feature. Run on CI nightly, not on every PR.
integration_test/
├── login_flow_test.dart
└── booking_flow_test.dart
Stub the API at the Dio level (custom HttpClientAdapter) rather than running against a real backend — real-backend integration tests are flaky and slow.
Coverage targets
- Repositories & notifiers: aim for 90%+ — these encode business rules.
- Widgets: test behavior, not coverage %. A widget can be 100% covered and still useless if it only verifies "the widget renders without throwing."
- Generated code: excluded entirely (
--no-test-randomize-ordering-seed --coverage with package:test_coverage excludes).
Anti-patterns to reject
- Tests that read implementation details (
expect(notifier._internalCache, ...)).
Future.delayed to "wait for async work" — use pumpAndSettle or expose a completer.
- Mocking the framework (
MockBuildContext, MockWidgetRef) — restructure the code instead.
- Tests that depend on test execution order.
- Skipping (
skip: 'flaky') instead of fixing — flaky tests rot the suite.
- Goldens committed without a CI guard that they were generated on the canonical platform.