Writing Widget Tests
1. Writing Widget Tests
Example: Basic Widget Test
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
});
}
| Method | Description | Usage |
|---|---|---|
| testWidgets | Entry point for widget tests | testWidgets('Test name', (WidgetTester tester) async { ... }); |
| tester.pumpWidget | Builds and mounts a widget tree | await tester.pumpWidget(MyApp()); |
| find | Locates widgets in the widget tree | find.text('Hello') |
| tester.tap | Simulates a tap on a widget | await tester.tap(find.byType(ElevatedButton)); |
| tester.enterText | Enters text into a TextField | await tester.enterText(find.byType(TextField), 'Hello'); |
Note: Widget tests run in a simulated environment and do not require an actual device or emulator.
Example: Testing Button Tap
testWidgets('Button tap increments counter', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Tap the button
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Verify the counter updated
expect(find.text('1'), findsOneWidget);
});
Example: Testing Text Input
testWidgets('TextField updates text', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Enter text into the field
await tester.enterText(find.byType(TextField), 'Flutter');
await tester.pump();
// Verify the text is updated
expect(find.text('Flutter'), findsOneWidget);
});
2. Using WidgetTester Class
The WidgetTester class is the core utility for writing widget tests in Flutter. It provides methods to build, interact with, and verify the state of widgets in a test environment.
| Method | Description | Example |
|---|---|---|
| pump() | Builds and renders the widget tree. Can be called with or without a duration to simulate time passage. | |
| pumpWidget() | Builds and renders a widget tree. Typically used once at the beginning of a test. | await tester.pumpWidget(MyApp()); |
| tap() | Simulates a tap on a widget. Requires a Finder to locate the target widget. |
await tester.tap(find.text('Submit')); |
| enterText() | Enters text into a TextField. Requires a Finder to locate the field. |
await tester.enterText(find.byType(TextField), 'Hello World'); |
| text() | Retrieves the text content of a widget. Useful for verifying that widgets display expected values. | final text = tester.text(find.text('Hello World')); |
| widget() | Retrieves a widget from the widget tree. Useful for inspecting properties of a specific widget. | final widget = tester.widget(find.byType(Text)); |
Example: Basic Widget Test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build the widget
await tester.pumpWidget(MaterialApp(home: Text('Hello World')));
// Verify the text
expect(find.text('Hello World'), findsOneWidget);
});
}
3. Finding Widgets
| Method | Description | Usage |
|---|---|---|
| find.byType() | Finds widgets of a specific type | final finder = find.byType(Text); |
| find.byKey() | Finds widgets with a specific key | final finder = find.byKey(Key('myKey')); |
| find.byIcon() | Finds widgets with a specific icon | final finder = find.byIcon(Icons.add); |
| find.byText() | Finds widgets with specific text content | final finder = find.byText('Hello World'); |
| find.byTooltip() | Finds widgets with a specific tooltip | final finder = find.byTooltip('Save'); |
| find.bySemanticsLabel() | Finds widgets with a specific semantic label | final finder = find.bySemanticsLabel('Submit'); |
Example: Using find.byType()
testWidgets('Find Text widget', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
final textFinder = find.byType(Text);
expect(textFinder, findsOneWidget);
});
Example: Using find.byKey()
testWidgets('Find widget by key', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
final keyFinder = find.byKey(Key('saveButton'));
expect(keyFinder, findsOneWidget);
});
Example: Using find.byText()
testWidgets('Find widget by text', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
final textFinder = find.byText('Welcome');
expect(textFinder, findsOneWidget);
});
4. Testing User Interactions
Example: Tapping a Button
testWidgets('taps button and updates text', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
final button = find.text('Tap Me');
expect(find.text('Hello'), findsNothing);
await tester.tap(button);
await tester.pumpAndSettle();
expect(find.text('Hello'), findsOneWidget);
});
| Method | Description | Notes |
|---|---|---|
| tester.tap() | Taps a widget | Triggers onTap callback |
| tester.doubleTap() | Double taps a widget | Triggers onDoubleTap callback |
| tester.longPress() | Long presses a widget | Triggers onLongPress callback |
| tester.drag() | Drags a widget | Triggers onDrag callbacks |
Example: Dragging a Widget
testWidgets('drags slider', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
final slider = find.byType(Slider);
await tester.drag(slider, const Offset(50, 0));
await tester.pumpAndSettle();
expect(find.text('Value: 50'), findsOneWidget);
});
Example: Typing in a TextField
testWidgets('types text into field', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
final textField = find.byType(TextField);
await tester.enterText(textField, 'Hello World');
await tester.pumpAndSettle();
expect(find.text('Hello World'), findsOneWidget);
});
Note: Use
tester.pumpAndSettle() after user interaction to ensure the UI updates before assertions.
Common Interaction Patterns
- Tap: Use
tester.tap()to simulate button presses - Drag: Use
tester.drag()to simulate swipe or slider movements - Type: Use
tester.enterText()to simulate text input - Double Tap: Use
tester.doubleTap()for double-click scenarios - Long Press: Use
tester.longPress()for context menu triggers
5. Verifying Widget Properties
| Property | Values | Description |
|---|---|---|
| key | Key |
Unique identifier for a widget in the widget tree |
| child | Widget |
The child widget to be tested |
| text | String |
The text content to verify in a widget |
| visible | bool |
Whether the widget is expected to be visible on screen |
Example: Verifying Widget Properties
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Verify widget properties', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Text('Hello World'),
),
),
);
// Verify text content
expect(find.text('Hello World'), findsOneWidget);
// Verify visibility
expect(find.text('Hello World'), findsOneWidget);
});
}
6. Testing Widget Rebuilds
Testing widget rebuilds in Flutter ensures that your widgets update correctly when their state changes. This is crucial for maintaining UI consistency and responsiveness.
| Method | Description | Usage |
|---|---|---|
| tester.pump() | Triggers a rebuild of the widget tree | await tester.pump(); |
| tester.pumpWidget() | Rebuilds the widget tree with a new widget | await tester.pumpWidget(newWidget); |
| tester.pumpAndSettle() | Continues pumping until no more rebuilds occur | await tester.pumpAndSettle(); |
Example: Testing Rebuilds with State Changes
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Rebuilds when counter changes', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CounterWidget(),
),
),
);
// Initial state
expect(tester.text(find.text('0')), findsOneWidget);
// Simulate a state change
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Verify rebuild
expect(tester.text(find.text('1')), findsOneWidget);
});
}
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(count.toString()),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text('Increment'),
),
],
);
}
}
Note: Use
tester.pumpAndSettle() to ensure all animations and async operations complete before asserting the final state.
Warning: Avoid relying on
tester.pump() without any arguments in tests, as it may not guarantee a full rebuild in all cases.
7. Using Finders and Matchers
| Finder | Description | Usage Example |
|---|---|---|
| find.byType(WidgetType) | Finds widgets of a specific type | find.byType(Text) |
| find.byKey(Key) | Finds widgets with a specific key | find.byKey(const Key('myKey')) |
| find.byWidget(Widget) | Finds a specific widget instance | find.byWidget(MyWidget()) |
| find.byText(String) | Finds widgets with specific text content | find.byText('Hello World') |
| find.byIcon(IconData) | Finds widgets with a specific icon | find.byIcon(Icons.add) |
| find.byTooltip(String) | Finds widgets with a specific tooltip | find.byTooltip('Save') |
Example: Using Finders in a Test
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';
void main() {
testWidgets('Verify text is displayed', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Find a Text widget with specific content
expect(find.byText('Welcome'), findsOneWidget);
});
}
| Matcher | Description | Usage Example |
|---|---|---|
| findsOneWidget | Matches exactly one widget | expect(find.byType(Text), findsOneWidget) |
| findsNothing | Matches no widgets | expect(find.byType(NonExistentWidget), findsNothing) |
| findsNWidgets(int count) | Matches exactly N widgets | expect(find.byType(Text), findsNWidgets(3)) |
| findsAtLeastOneWidget | Matches one or more widgets | expect(find.byType(Text), findsAtLeastOneWidget) |
| hasText(String text) | Matches widgets containing specific text | expect(find.byType(Text).first, hasText('Hello')) |
| isA<WidgetType>() | Matches widgets of a specific type | expect(find.byType(Text), isA<Text>()) |
Example: Using Matchers in a Test
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';
void main() {
testWidgets('Verify widget count', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// Check that there are exactly 3 Text widgets
expect(find.byType(Text), findsNWidgets(3));
});
}
Common Finder and Matcher Patterns
find.byType(WidgetType)is useful for verifying widget presencefind.byKey(Key)is essential for testing widgets with keysfind.byText(String)is the most common way to find UI elements by contentfindsOneWidgetensures only one widget matches the criteriafindsNWidgets(int count)is useful for verifying collections or lists
8. Testing Async Widgets
| Method | Description | Usage |
|---|---|---|
| pumpAndSettle() | Waits for all async operations to complete | await tester.pumpAndSettle(); |
| pump() | Triggers a widget rebuild | await tester.pump(const Duration(milliseconds: 500)); |
| pumpWidget() | Initializes the widget under test | await tester.pumpWidget(MyWidget()); |
Note: Use
pumpAndSettle() to ensure all async operations (e.g., API calls) are completed before asserting results.Example: Testing Async Data Fetch
testWidgets('Fetches and displays data', (WidgetTester tester) async {
final mockData = {'name': 'Flutter'};
when(mockApi.fetchData()).thenAnswer((_) async => mockData);
await tester.pumpWidget(MyAsyncWidget(api: mockApi));
await tester.pumpAndSettle();
expect(find.text('Flutter'), findsOneWidget);
});
9. Using Golden Tests
Golden tests in Flutter are used to verify that the visual appearance of widgets matches a known "golden" image. They are particularly useful for ensuring UI consistency and catching unintended visual changes.
| Property | Description | Example |
|---|---|---|
| goldenFile | Path to the golden file used for comparison. | test/goldens/my_widget.png |
| testFile | Path to the test file that contains the golden test. | test/my_widget_test.dart |
| widget | The widget to be tested for visual appearance. | MyWidget() |
Example: Basic Golden Test Setup
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('MyWidget golden test', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
await expectLater(
find.byType(MyWidget),
matchesGoldenFile('golden/my_widget.png'),
);
});
}
Note: Golden tests require the golden file to be generated first using
flutter test --update-goldens.
Warning: Golden tests can be brittle due to platform differences and rendering variations. Ensure tests are run in a consistent environment.
Golden Test Workflow
- Create a new test file for the widget.
- Use
matchesGoldenFileto compare the widget’s output with a golden image. - Run
flutter test --update-goldensto generate the initial golden file. - Re-run tests to ensure visual consistency.
10. Mocking Dependencies in Widget Tests
| Dependency | Mocking Approach | Notes |
|---|---|---|
| HTTP Client | MockHttpClient or Dio with MockAdapter |
Use http package or dio with mock adapters to simulate API responses |
| Database | mockito or sqflite_mock |
Mock database queries and responses to isolate widget logic |
| Shared Preferences | mock_shared_preferences |
Simulate key-value storage without actual persistence |
Example: Mocking HTTP Client
// Using mockito for HTTP client mocking
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
class MockHttpClient extends Mock implements http.Client {}
// In test
final mockClient = MockHttpClient();
when(mockClient.get(any)).thenAnswer(
(_) async => http.Response('{"name": "Flutter"}', 200),
);
// Use mockClient in widget test
Note: Use
mockito or mocktail for clean and maintainable mocks.
Warning: Avoid real network calls in widget tests to ensure fast and reliable test execution.