F
flutter-add-widget-test
About flutter-add-widget-test
A practical guide for Flutter developers adding widget tests. It covers finders, interactions, frame pumping, BLoC tests, common pitfalls, and a step-by-step workflow.
Key features
- 01Widget testing core APIs
- 02Key-based widget finders
- 03Tap, text, and scroll interactions
- 04Animation and time pumping strategies
- 05BLoC and Cubit testing
- 06Common issue troubleshooting
Use cases
- 01Test state changes in interactive controls
- 02Validate form input and errors
- 03Test off-screen items in long lists
- 04Diagnose animation or layout test failures
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ dhruvanbhalara/ skills --skill flutter-add-widget-testAfter installing, type/flutter-add-widget-testin Claude Code to confirm it loaded.
Details
From the project READMEContents
- Setup
- Core APIs
- Finder Patterns
- Interaction Patterns
- Pump Strategies
- Testing with BLoC
- Common Pitfalls
- Workflow: Adding a Widget Test
- Examples
Setup
flutter_testis an SDK dependency, so nopub addis needed.- Test file naming:
test/<mirror_path>/<widget>_test.dart. - Every test file starts with:
import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - Use
testWidgets('description', (WidgetTester tester) async { ... })for all widget tests. - Always wrap the widget under test in
MaterialAppto provideMediaQuery,Theme, andNavigatorcontext.
Core APIs
| API | Purpose |
|---|---|
tester.pumpWidget(widget) | Render widget into test environment |
tester.pump() | Trigger a single frame |
tester.pump(Duration(...)) | Advance by specific duration |
tester.pumpAndSettle() | Wait for all animations to complete |
tester.tap(finder) | Simulate tap gesture |
tester.longPress(finder) | Simulate long press |
tester.enterText(finder, 'text') | Type into text field |
tester.drag(finder, Offset(dx, dy)) | Simulate drag gesture |
tester.scrollUntilVisible(finder, delta) | Scroll until widget is visible |
Finder Patterns
Use finders to locate widgets in the test tree. Prefer Key-based finders for stability.
find.byKey(const ValueKey('login_button')): Preferred. Most stable across refactors.find.byType(ElevatedButton): By widget type. Fails if multiple instances exist.find.text('Submit'): By displayed text. Avoid with localized strings.find.byIcon(Icons.add): By icon data.find.descendant(of: parentFinder, matching: childFinder): Nested lookup.find.ancestor(of: childFinder, matching: parentFinder): Reverse lookup.
Key Naming Convention: Use Key('feature_action_id') format on interactive widgets.
// Production code
ElevatedButton(
key: const Key('login_submit_button'),
onPressed: _onSubmit,
child: const Text('Login'),
)
// Test code
final submitButton = find.byKey(const Key('login_submit_button'));
await tester.tap(submitButton);
Interaction Patterns
Tap and Verify State Change
testWidgets('increments counter on tap', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byKey(const Key('increment_button')));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
Enter Text and Validate Form
testWidgets('validates email field', (tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginForm()));
await tester.enterText(find.byKey(const Key('email_field')), 'invalid');
await tester.tap(find.byKey(const Key('submit_button')));
await tester.pumpAndSettle();
expect(find.text('Enter a valid email'), findsOneWidget);
});
Scroll to Off-Screen Widget
testWidgets('finds item in long list', (tester) async {
await tester.pumpWidget(const MaterialApp(home: ItemListPage()));
final listFinder = find.byType(Scrollable);
final itemFinder = find.byKey(const Key('item_99'));
await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder);
expect(itemFinder, findsOneWidget);
});
Pump Strategies
Choose the right pump method based on your scenario:
| Scenario | Method | Why |
|---|---|---|
| Simple state change | pump() | Single frame is enough |
| Animation completes | pumpAndSettle() | Waits for all frames |
| Timed animation | pump(Duration(milliseconds: 300)) | Advance specific time |
Infinite animation (e.g., CircularProgressIndicator) | pump() | pumpAndSettle() will timeout |
| Debounced input | pump(Duration(milliseconds: 500)) | Wait for debounce period |
WARNING: pumpAndSettle() throws PumpAndSettleTimedOutException on infinite animations. Use pump() instead when testing loading states.
Testing with BLoC
When testing widgets that depend on BLoC/Cubit:
testWidgets('shows user name from BLoC', (tester) async {
final mockBloc = MockUserBloc();
whenListen(
mockBloc,
Stream.fromIterable([UserLoaded(User(name: 'Alice'))]),
initialState: UserInitial(),
);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<UserBloc>.value(
value: mockBloc,
child: const UserProfilePage(),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
});
- Use
MockBloc/MockCubitfrombloc_testpackage. - Use
whenListen()to stub state stream responses. - Use
BlocProvider.value()to inject mock into the widget tree.
Common Pitfalls
| Error | Cause | Fix |
|---|---|---|
No MediaQuery widget ancestor | Missing MaterialApp wrapper | Wrap in MaterialApp(home: ...) |
A RenderFlex overflowed | Widget exceeds test viewport | Constrain with SizedBox or Expanded |
Vertical viewport was given unbounded height | ListView without height constraint | Wrap in SizedBox(height: 600) |
| Widget not found after navigation | Missing pumpAndSettle() | Add await tester.pumpAndSettle() after navigation |
PumpAndSettleTimedOutException | Infinite animation running | Use pump() instead of pumpAndSettle() |
Workflow: Adding a Widget Test
Task Progress
- Step 1: Add
Keys to interactive widgets in production code. - Step 2: Create test file at
test/<mirror_path>/<widget>_test.dart. - Step 3: Wrap widget in
MaterialAppand calltester.pumpWidget(). - Step 4: Use appropriate finder (
byKeypreferred). - Step 5: Simulate interactions (
tap,enterText,drag). - Step 6: Assert with
expect(finder, findsOneWidget)or state checks. - Step 7: Apply Golden Variant / State Matrix / Interaction Contract pattern (see
flutter-testing). - Step 8: Run
flutter test test/path/to/widget_test.dart. - Step 9: Feedback Loop: fix failures → re-run until green.
Examples
Minimal Widget Test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/features/counter/counter_page.dart';
void main() {
group('$CounterPage', () {
testWidgets('renders initial counter value', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
expect(find.byType(FloatingActionButton), findsOneWidget);
});
testWidgets('increments counter when FAB is tapped', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
});
}
Comments
Markdown supported