This is a comprehensive testing guide for Flutter projects that covers the full stack: unit tests, widget tests, integration tests, and Riverpod provider testing. It pushes a clear architecture where you test each layer in isolation using Given-When-Then structure, mock dependencies but never providers (override them instead), and always reset GetIt in tearDown. The reference tables are genuinely useful, especially the quick lookup for what to mock at each layer and common mistakes like forgetting to set screen size in widget tests or using Future.delayed instead of pumpAndSettle. If your Flutter codebase uses Riverpod and GetIt, this will save you from rewriting the same test setup patterns.
npx -y skills add harishwarrior/flutter-claude-skills --skill flutter-tester --agent claude-codeInstalls into .claude/skills of the current project.
flutter_test dependencydart run build_runner build to generate mocks after adding @GenerateMocks annotationsfvm flutter test instead of flutter test)Test each architectural layer in isolation using Given-When-Then structure. Always test both success and error paths. Never mock providers — override their dependencies instead.
Load the relevant file based on what you're testing:
| What you're testing | Reference file |
|---|---|
| Repository, DAO, Service logic | references/layer_testing_patterns.md |
| Widget UI, interactions, dialogs, navigation | references/widget_testing_guide.md |
| Riverpod provider state, mutations, lifecycle | references/riverpod_testing_guide.md |
Test each layer against its own mocked dependencies:
| Layer | What to test | What to mock |
|---|---|---|
| Repository | Data coordination between sources | DAOs, APIs, Logger |
| DAO | Database CRUD operations | Use real in-memory DB, mock Logger |
| Provider | State management and transitions | Services, Repositories |
| Service | Business logic and workflows | Repositories, Network clients |
| Widget | UI behaviour and interactions | Provider dependencies (via overrides) |
test('Given valid data, When fetchUsers called, Then returns user list', () async {
// Arrange (Given)
when(mockDAO.fetchAll()).thenAnswer((_) async => expectedUsers);
// Act (When)
final result = await repository.fetchUsers();
// Assert (Then)
expect(result, equals(expectedUsers));
verify(mockDAO.fetchAll()).called(1);
});
group('UserRepository', () {
group('fetchUsers', () {
setUp(() { /* init mocks, register with GetIt */ });
tearDown(() => GetIt.I.reset()); // Always reset GetIt
test('Given success ... When ... Then ...', () { });
test('Given error ... When ... Then ...', () { });
});
});
@GenerateMocks([IUserDAO, IUserAPI, ILogger])
void main() { ... }
Run dart run build_runner build after modifying @GenerateMocks.
setUp(() {
mockDAO = MockIUserDAO();
mockLogger = MockILogger();
GetIt.I
..registerSingleton<IUserDAO>(mockDAO)
..registerSingleton<ILogger>(mockLogger);
});
tearDown(() => GetIt.I.reset()); // Critical — always reset
class FakeLogger extends ILogger) — silent stubs; use when you don't need to verify callsMockILogger) — use when you need when(), verify(), or thenThrow()| Scenario | Key pattern |
|---|---|
| Test a repository | Mock DAO + API → inject into repository constructor |
| Test a DAO | FakeDatabase or openInMemoryDatabase() in setUp, delete table in tearDown |
| Test a Riverpod provider | createContainer(overrides: [serviceProvider.overrideWith(...)]) |
| Test a widget | Set screen size, use find.byKey(), call pumpAndSettle() |
| Test a loading state | Use Completer, pump() to assert loading, complete, pump() again |
| Test platform-specific UI | debugDefaultTargetPlatformOverride = TargetPlatform.iOS — reset after |
| Test GoRouter navigation | FakeGoRouter + MockGoRouterProvider |
flutter test --coverage # All tests with coverage
flutter test test/path/to/test.dart # Specific file
flutter test --plain-name "Given valid data" # Filter by name
genhtml coverage/lcov.info -o coverage/html # Generate HTML coverage report
# Prefix any command with `fvm` if using Flutter Version Manager
| Mistake | Fix |
|---|---|
| Mocking a provider directly | Override its dependencies: provider.overrideWith(...) |
Missing GetIt.I.reset() in tearDown | Tests pollute each other — always reset |
await Future.delayed() in tests | Use await tester.pumpAndSettle() or Completer instead |
| Finding widgets by text string | Use find.byKey(const Key('name')) — stable across text changes |
| No screen size in widget tests | Add tester.view.physicalSize = const Size(1000, 1000) |
Not resetting debugDefaultTargetPlatformOverride | Set to null at the end of the test |
tearDown() without a lambda | Write tearDown(() async { ... }) not tearDown() async { ... } |
Setup & Mocking:
GetIt.I.reset() in tearDowntearDowntearDownWidget Tests:
find.byKey()physicalSize + devicePixelRatio)debugDefaultTargetPlatformOverride = null)Test Coverage:
Future.delayed)Code Quality:
verify() or verifyNever() where appropriatesickn33/antigravity-awesome-skills
wshobson/agents
kotlin/kotlin-agent-skills