Testing
Flutter testing has three layers: unit (plain Dart), widget (no real device), and integration (real device). Use mocktail/Mockito for mocks, and run goldens for visual snapshots. The pyramid scales: many unit tests, fewer widget tests, very few integration tests.
Unit, widget, golden, and integration patterns
EXAMPLE
// pubspec.yaml (test deps)
// dev_dependencies:
// flutter_test:
// sdk: flutter
// mocktail: ^1.0.0
// integration_test:
// sdk: flutter
// ===== 1) Pure Dart unit test =====
import 'package:flutter_test/flutter_test.dart';
int totalCents(List<Map<String, dynamic>> items) =>
items.fold(0, (s, i) => s + (i['qty'] as int) * (i['priceCents'] as int));
void main() {
group('totalCents', () {
test('sums all items', () {
expect(totalCents([
{'qty': 2, 'priceCents': 250},
{'qty': 1, 'priceCents': 500},
]), 1000);
});
test('returns 0 for empty list', () {
expect(totalCents([]), 0);
});
});
}
// ===== 2) Widget test =====
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int n = 0;
@override
Widget build(BuildContext c) {
return Material(
child: Column(children: [
Text('Count: $n'),
ElevatedButton(onPressed: () => setState(() => n++), child: const Text('+')),
]),
);
}
}
void main_widget() {
testWidgets('counter increments on tap', (tester) async {
await tester.pumpWidget(const MaterialApp(home: Counter()));
expect(find.text('Count: 0'), findsOneWidget);
await tester.tap(find.text('+'));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
});
}
// ===== 3) Mocking with mocktail =====
import 'package:mocktail/mocktail.dart';
abstract class Api { Future<String> getName(String id); }
class MockApi extends Mock implements Api {}
void main_mock() {
test('shows fetched name', () async {
final api = MockApi();
when(() => api.getName('u1')).thenAnswer((_) async => 'Alice');
expect(await api.getName('u1'), 'Alice');
verify(() => api.getName('u1')).called(1);
});
}
// ===== 4) Golden tests (visual snapshots) =====
void main_golden() {
testWidgets('counter golden', (tester) async {
await tester.pumpWidget(const MaterialApp(home: Counter()));
await expectLater(find.byType(Counter), matchesGoldenFile('counter.png'));
});
}
// Run: flutter test --update-goldens (regenerate goldens after intentional UI change)
// CI runs without --update-goldens and fails on visual diff.
// ===== 5) Integration test (real device / emulator) =====
// integration_test/app_test.dart
// import 'package:flutter_test/flutter_test.dart';
// import 'package:integration_test/integration_test.dart';
// import 'package:my_app/main.dart' as app;
//
// void main() {
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
//
// testWidgets('full happy path', (tester) async {
// app.main();
// await tester.pumpAndSettle();
// await tester.tap(find.text('Sign in'));
// await tester.pumpAndSettle();
// // ... fill form, submit, assert next screen
// });
// }
//
// Run on a device/emulator:
// flutter test integration_test
// ===== 6) Patterns to internalise =====
// - Unit tests first; widget tests for screens; integration tests for happy paths
// - Inject dependencies (api, clock) into widgets via constructor or InheritedWidget
// - Use 'pumpAndSettle' sparingly (it can hang on infinite animations); prefer 'pump' with explicit durations
// - Mock at the boundary; do not over-mock framework widgets
// ===== Pitfalls =====
// - testWidgets without await tester.pumpWidget(...) -> blank test
// - Animations + pumpAndSettle infinite loop -> use pump(duration)
// - Goldens that include OS-rendered fonts; pin a font via test setup
// - Async work not awaited inside tester.runAsync -> false-pass tests
Why it matters
Build the testing pyramid: many unit tests, fewer widget tests, very few integration tests. Unit tests catch logic bugs in milliseconds; widget tests prove the UI wires up; integration exists to prove "the whole thing still works" on a real device once per deploy. Invert the pyramid and you get slow flaky CI that nobody trusts.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
void main() {
testWidgets('counter increments', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
}
Try it Yourself »
Discussion
Loading…