// A comprehensive Flutter testing skill for creating, writing, and analyzing tests in any Flutter project. Provides guidance on test structure, mocking patterns, Riverpod testing, widget testing, and industry best practices for reliable, maintainable tests.
| name | flutter-tester |
| description | A comprehensive Flutter testing skill for creating, writing, and analyzing tests in any Flutter project. Provides guidance on test structure, mocking patterns, Riverpod testing, widget testing, and industry best practices for reliable, maintainable tests. |
This skill provides comprehensive guidance for writing consistent, reliable, and maintainable tests for Flutter applications. Follow the testing patterns, mocking strategies, and architectural guidelines to ensure tests are isolated, repeatable, and cover both success and error scenarios. This skill works with any Flutter project using common packages like Riverpod, Mockito, and flutter_test.
Use this skill when:
Test each layer in isolation:
Always structure tests using Given-When-Then pattern:
test('Given valid data, When operation executes, Then returns expected result', () async {
// Arrange (Given)
when(mockDAO.getData()).thenAnswer((_) async => testData);
// Act (When)
final result = await repository.fetchData();
// Assert (Then)
expect(result, equals(testData));
verify(mockDAO.getData()).called(1);
});
group() blockssetUp() for common initializationtearDown() for cleanup (reset GetIt, dispose resources)setUpAll() for one-time expensive setupDetermine which architectural layer you're testing:
@GenerateMocks([ILogger, ICarouselRepository, INotificationDAO])
void main() {
// Test code
}
Important: Never mock providers directly. Override their dependencies instead.
setUp(() {
mockLogger = MockILogger();
mockRepository = MockICarouselRepository();
GetIt.I.registerSingleton<ILogger>(mockLogger);
GetIt.I.registerSingleton<ICarouselRepository>(mockRepository);
});
tearDown(() => GetIt.I.reset());
setUpAll(() async {
SharedPreferences.setMockInitialValues({'key1': 'value1'});
SharedPrefManager.instance = await SharedPreferences.getInstance();
});
Refer to the references/layer_testing_patterns.md file for detailed examples of:
Always test both success and failure paths:
test('Given service throws exception, When called, Then logs error and returns fallback', () async {
// Arrange
final exception = Exception('Network error');
when(mockService.fetchData()).thenThrow(exception);
// Act
final result = await repository.getData();
// Assert
expect(result, isEmpty); // Or appropriate fallback
verify(mockLogger.writeExceptionLog('RepositoryName', 'getData', exception, any)).called(1);
});
testWidgets('Test description', (tester) async {
tester.view.physicalSize = const Size(1000, 1000);
tester.view.devicePixelRatio = 1.0;
// Your test code
});
In source code:
ElevatedButton(
key: const Key('saveButton'),
onPressed: () {},
child: const Text('Save'),
);
In test:
await tester.tap(find.byKey(const Key('saveButton')));
await tester.pumpAndSettle();
If a key doesn't exist in the source widget, add it before writing the test.
when(mockService.fetchData()).thenAnswer((_) async => data);
await tester.pumpWidget(createTestWidget());
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pumpAndSettle();
expect(find.byType(DataWidget), findsOneWidget);
testWidgets('iOS specific behavior', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(createTestWidget());
expect(find.byType(CupertinoButton), findsOneWidget);
debugDefaultTargetPlatformOverride = null;
});
final container = createContainer(overrides: [
repoProvider.overrideWith((ref) => mockRepo),
]);
Use the createContainer() helper from test/riverpod_container.dart which auto-disposes on tearDown.
test('Given valid data, When state updates, Then reflects new value', () async {
final notifier = container.read(provider.notifier);
notifier.updateState(newValue);
expect(container.read(provider).value!.property, newValue);
});
test('Given empty data, When building initial state, Then returns default state', () async {
when(mockService.fetchData()).thenAnswer((_) async => []);
final container = createContainer();
final state = await container.read(provider.notifier).future;
expect(state.data, isEmpty);
expect(state.isLoading, false);
});
when(mockRepo.fetchFromDb()).thenAnswer((_) async => mockData);
when(mockApi.updateData(any, any, any)).thenAnswer((_) async => true);
when(mockRepo.fetchFromDb()).thenThrow(Exception('DB error'));
when(mockApi.updateData(any, any, any)).thenAnswer((_) async => false);
final completer = Completer<RegistrationModel>();
when(mockRepo.fetchData(any, any)).thenAnswer((_) => completer.future);
await tester.tap(find.text('Save'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
completer.complete(const RegistrationModel(status: 'success'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsNothing);
Use fake implementations for consistent dummy behavior:
class FakeLogger extends ILogger {
@override
void writeInfoLog(String className, String method, String message) {}
@override
void writeErrorLog(String className, String method, dynamic error, StackTrace? stack, [String? msg]) {}
}
Register in test setup:
GetIt.I.registerSingleton<ILogger>(FakeLogger.i);
Use mocks when you need to verify method calls or setup specific behaviors:
when(mockLogger.writeErrorLog(any, any, any, any)).thenReturn(null);
verify(mockLogger.writeErrorLog('ClassName', 'methodName', exception, any)).called(1);
late MenuDAO menuDAO;
late Database db;
late IDatabase mockDatabase;
setUp(() async {
await FakePathProviderPlatform.initialize();
PathProviderPlatform.instance = FakePathProviderPlatform();
mockDatabase = FakeDatabase();
db = await mockDatabase.database;
menuDAO = MenuDAO(
dbManager: mockDatabase,
logger: mockLogger,
);
});
tearDown() async {
await menuDAO.deleteTable();
if (GetIt.I.isRegistered<IDatabase>()) {
await GetIt.I<IDatabase>().close();
}
await GetIt.I.reset();
await FakePathProviderPlatform.cleanup();
}
Before submitting tests, ensure:
Setup & Mocking:
Widget Tests:
Test Coverage:
Code Quality:
// Single call
verify(mockService.method()).called(1);
// Multiple calls
verify(mockService.method()).called(3);
// Never called
verifyNever(mockService.method());
// Ordered calls
verifyInOrder([
mockService.method1(),
mockService.method2(),
]);
import 'package:your_app/path/to/global_variables.dart' as global_variables;
setUp(() {
global_variables.someGlobalVariable = initialValue;
});
tearDown(() {
global_variables.someGlobalVariable = initialValue; // Reset to default
});
testWidgets('Given provider disposed, When container disposed, Then unsubscribes and cleans up', () async {
final container = createContainer();
final notifier = container.read(provider.notifier);
await notifier.future;
container.dispose();
verify(mockService.unsubscribe(any, any)).called(1);
verify(mockService.dispose(any)).called(1);
});
flutter test --coverage
# Or if using FVM:
fvm flutter test --coverage
flutter test test/path/to/your_test.dart
# Or if using FVM:
fvm flutter test test/path/to/your_test.dart
flutter test --plain-name "Given valid data"
# Or if using FVM:
fvm flutter test --plain-name "Given valid data"
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
# Or if using FVM:
fvm flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
Widget createTestWidget(Widget child) {
return MaterialApp(
home: Scaffold(
body: child,
),
);
}
// With Riverpod
Widget createTestWidgetWithProviders(Widget child, List<Override> overrides) {
return ProviderScope(
overrides: overrides,
child: MaterialApp(
home: Scaffold(
body: child,
),
),
);
}
ProviderContainer createContainer({List<Override> overrides = const []}) {
final container = ProviderContainer(overrides: overrides);
addTearDown(container.dispose);
return container;
}
// Find by type
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Find by text
expect(find.text('Hello'), findsOneWidget);
// Find by key
expect(find.byKey(const Key('myKey')), findsOneWidget);
// Find descendant
expect(
find.descendant(
of: find.byType(Container),
matching: find.text('Child'),
),
findsOneWidget,
);
// Find ancestor
expect(
find.ancestor(
of: find.text('Child'),
matching: find.byType(Container),
),
findsOneWidget,
);
// Common matchers
expect(value, equals(expected));
expect(value, isNotNull);
expect(value, isNull);
expect(list, isEmpty);
expect(list, isNotEmpty);
expect(list, hasLength(3));
expect(list, contains('item'));
expect(list, containsAll(['a', 'b']));
expect(value, greaterThan(5));
expect(value, lessThan(10));
expect(value, inRange(1, 10));
// Custom matchers
expect(find.byType(Widget), findsOneWidget);
expect(find.byType(Widget), findsNothing);
expect(find.byType(Widget), findsWidgets);
expect(find.byType(Widget), findsNWidgets(3));
setUp(() {
GetIt.I.registerSingleton<ApiService>(mockApiService);
GetIt.I.registerLazySingleton<UserRepository>(() => UserRepository());
});
tearDown(() {
GetIt.I.reset(); // Always reset GetIt between tests
});
test('Given stream emits values, When listening, Then receives all values', () async {
// Arrange
final streamController = StreamController<int>();
final values = <int>[];
// Act
streamController.stream.listen(values.add);
streamController.add(1);
streamController.add(2);
streamController.add(3);
await streamController.close();
// Wait for stream to complete
await Future.delayed(Duration.zero);
// Assert
expect(values, equals([1, 2, 3]));
});
testWidgets('Given timer completes, When countdown finishes, Then shows message', (tester) async {
await tester.pumpWidget(MyTimerWidget());
// Fast-forward time
await tester.pump(const Duration(seconds: 5));
expect(find.text('Time is up!'), findsOneWidget);
});
testWidgets('Given long list, When scrolling, Then finds bottom item', (tester) async {
await tester.pumpWidget(MyLongListWidget());
// Scroll until item is visible
await tester.scrollUntilVisible(
find.text('Item 99'),
500.0,
);
expect(find.text('Item 99'), findsOneWidget);
});
This skill includes reference files with detailed patterns and examples:
layer_testing_patterns.md - Comprehensive examples for testing repositories, providers, DAOs, and serviceswidget_testing_guide.md - Detailed widget testing patterns with keys, screen size, and user interactionsriverpod_testing_guide.md - Advanced Riverpod provider testing patterns and state management testingRefer to these references when you need specific implementation examples or encounter complex testing scenarios.