Widget tests run in milliseconds without a device and catch UI regressions before users do.
Flutter supports three kinds of tests:
- Unit tests — plain Dart logic (view models, repositories, validators) with
package:testAPIs:test,expect,group. - Widget tests — render widgets in a headless test environment with
testWidgetsand a `WidgetTester`. Fast, no device needed. - Integration tests —
integration_testpackage; run the full app on a device or emulator, for end-to-end flows.
A widget test:
testWidgets('increments', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});Key APIs:
pumpWidgetbuilds the widget; wrap it inMaterialApp(and providers) as needed.pump()triggers a frame after an interaction;pump(duration)advances time;pumpAndSettle()pumps until animations finish (it times out with infinite animations).- Finders:
find.text,find.byType,find.byKey(ValueKey('save')),find.byIcon,find.widgetWithText. - Matchers:
findsOneWidget,findsNothing,findsNWidgets(n). - Interactions:
tap,enterText,drag,longPress.
Inject fakes for repositories through constructors or providers instead of real HTTP. Real network calls in widget tests return 400 by default. Golden tests (matchesGoldenFile) compare rendered pixels to a stored image. Run with flutter test, and flutter test --coverage for coverage.
Example
// test/like_button_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
int likes = 0;
@override
Widget build(BuildContext context) => TextButton(
key: const ValueKey('like'), onPressed: () => setState(() => likes++), child: Text('$likes likes'));
}
void main() {
testWidgets('tapping like increments the count', (tester) async {
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: LikeButton())));
expect(find.text('0 likes'), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('like')));
await tester.pump(); // rebuild after setState
expect(find.text('1 likes'), findsOneWidget);
expect(find.text('0 likes'), findsNothing);
});
}Remember
- Unit tests for logic, widget tests for UI, integration tests for full flows
pumpWidget, interact, thenpump()before asserting- Find by key, text, type or icon; inject fakes instead of real HTTP
Watch out for
- Forgetting
await tester.pump()after a tap, so the expectation sees the old UI.