Writing Flutter Widget Tests
Contents
- Setup & Configuration
- Core Components
- Workflow: Implementing a Widget Test
- Interaction & State Management
- Examples
Setup & Configuration
Ensure the testing environment is properly configured before authoring widget tests.
- Add the
flutter_testdependency to thedev_dependenciessection ofpubspec.yaml. - Place all test files in the
test/directory at the root of the project. - Suffix all test file names with
_test.dart(e.g.,widget_test.dart).
Core Components
Utilize the following flutter_test components to interact with and validate the widget tree:
WidgetTester: The primary interface for building and interacting with widgets in the test environment. Provided automatically by thetestWidgets()function.Finder: Locates widgets in the test environment (e.g.,find.text('Submit'),find.byType(TextField),find.byKey(Key('submit_btn'))).Matcher: Verifies the presence or state of widgets located by aFinder(e.g.,findsOneWidget,findsNothing,findsNWidgets(2),matchesGoldenFile).
Workflow: Implementing a Widget Test
Copy the following checklist to track progress when implementing a new widget test.
Task Progress
- Step 1: Define the test. Use
testWidgets('description', (WidgetTester tester) async { ... }). - Step 2: Build the widget. Call
await tester.pumpWidget(MyWidget())to render the UI. Wrap the widget in aMaterialApporDirectionalitywidget if it requires inherited directional or theme data. - Step 3: Locate elements. Instantiate
Finderobjects for the target widgets. - Step 4: Verify initial state. Use
expect(finder, matcher)to validate the initial render. - Step 5: Simulate interactions. Execute gestures or inputs (e.g.,
await tester.tap(buttonFinder)). - Step 6: Rebuild the tree. Call
await tester.pump()orawait tester.pumpAndSettle()to process state changes. - Step 7: Verify updated state. Use
expect()to validate the UI after the interaction. - Step 8: Run and validate. Execute
flutter test test/your_test_file_test.dart. - Step 9: Feedback Loop. Review test output -> identify failing matchers -> adjust widget logic or test assertions -> re-run until passing.
Interaction & State Management
Apply the following conditional logic based on the type of interaction or state change being tested:
- If testing static rendering: Call
await tester.pumpWidget()once, then immediately runexpect()assertions. - If testing standard state changes (e.g., button taps):
- Call
await tester.tap(finder). - Call
await tester.pump()to trigger a single frame rebuild.
- Call
- If testing animations, transitions, or asynchronous UI updates:
- Trigger the action (e.g.,
await tester.drag(finder, Offset(500, 0))). - Call
await tester.pumpAndSettle()to repeatedly pump frames until no more frames are scheduled (animation completes).
- Trigger the action (e.g.,
- If testing text input: Call
await tester.enterText(textFieldFinder, 'Input string'). - If testing items in a dynamic or long list: Call
await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder)to ensure the target widget is rendered before interacting with it.
Examples
High-Fidelity Widget Test Implementation
Target Widget (lib/todo_list.dart):
dartimport 'package:flutter/material.dart'; class TodoList extends StatefulWidget { const TodoList({super.key}); @override State<TodoList> createState() => _TodoListState(); } class _TodoListState extends State<TodoList> { final todos = <String>[]; final controller = TextEditingController(); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Column( children: [ TextField(controller: controller), Expanded( child: ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return Dismissible( key: Key('$todo$index'), onDismissed: (_) => setState(() => todos.removeAt(index)), child: ListTile(title: Text(todo)), ); }, ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { todos.add(controller.text); controller.clear(); }); }, child: const Icon(Icons.add), ), ), ); } }
Test Implementation (test/todo_list_test.dart):
dartimport 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/todo_list.dart'; void main() { testWidgets('Add and remove a todo item', (WidgetTester tester) async { // 1. Build the widget await tester.pumpWidget(const TodoList()); // 2. Verify initial state expect(find.byType(ListTile), findsNothing); // 3. Enter text into the TextField await tester.enterText(find.byType(TextField), 'Buy groceries'); // 4. Tap the add button await tester.tap(find.byType(FloatingActionButton)); // 5. Rebuild the widget to reflect the new state await tester.pump(); // 6. Verify the item was added expect(find.text('Buy groceries'), findsOneWidget); // 7. Swipe the item to dismiss it await tester.drag(find.byType(Dismissible), const Offset(500, 0)); // 8. Build the widget until the dismiss animation ends await tester.pumpAndSettle(); // 9. Verify the item was removed expect(find.text('Buy groceries'), findsNothing); }); }


