mcpField
F
flutter-add-widget-test

flutter-add-widget-test

github.com/dhruvanbhalaraTesting & quality3051 installsUpdated 9w ago
Repository

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

  1. 01Widget testing core APIs
  2. 02Key-based widget finders
  3. 03Tap, text, and scroll interactions
  4. 04Animation and time pumping strategies
  5. 05BLoC and Cubit testing
  6. 06Common issue troubleshooting

Use cases

  1. 01Test state changes in interactive controls
  2. 02Validate form input and errors
  3. 03Test off-screen items in long lists
  4. 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-test

After installing, type/flutter-add-widget-testin Claude Code to confirm it loaded.

Details

From the project README

Contents

Setup

  • flutter_test is an SDK dependency, so no pub add is 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 MaterialApp to provide MediaQuery, Theme, and Navigator context.

Core APIs

APIPurpose
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:

ScenarioMethodWhy
Simple state changepump()Single frame is enough
Animation completespumpAndSettle()Waits for all frames
Timed animationpump(Duration(milliseconds: 300))Advance specific time
Infinite animation (e.g., CircularProgressIndicator)pump()pumpAndSettle() will timeout
Debounced inputpump(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 / MockCubit from bloc_test package.
  • Use whenListen() to stub state stream responses.
  • Use BlocProvider.value() to inject mock into the widget tree.

Common Pitfalls

ErrorCauseFix
No MediaQuery widget ancestorMissing MaterialApp wrapperWrap in MaterialApp(home: ...)
A RenderFlex overflowedWidget exceeds test viewportConstrain with SizedBox or Expanded
Vertical viewport was given unbounded heightListView without height constraintWrap in SizedBox(height: 600)
Widget not found after navigationMissing pumpAndSettle()Add await tester.pumpAndSettle() after navigation
PumpAndSettleTimedOutExceptionInfinite animation runningUse 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 MaterialApp and call tester.pumpWidget().
  • Step 4: Use appropriate finder (byKey preferred).
  • 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