Expert · Responsive design and testing · topic 41 of 50 · 35 min
Widget tests
Explanation
What a widget test checks
Unit tests check logic; widget tests check that the UI behaves: tapping + increases the number, an invalid form shows an error, an empty list shows a friendly message. They run in a simulated Flutter environment on your computer, so they are much faster than running the app, yet they exercise real widgets, layout and gestures.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chai_shop/counter_page.dart';
void main() {
testWidgets('tapping + increases the cup count', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('Cups sold today: 0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // rebuild after setState
expect(find.text('Cups sold today: 1'), findsOneWidget);
expect(find.text('Cups sold today: 0'), findsNothing);
});
}Finders
| Finder | Finds |
|---|---|
| find.text('Pay') | Widgets showing exactly that text |
| find.byType(TextFormField) | Widgets of a type |
| find.byKey(const Key('mobileField')) | A widget with a specific key (stable across text changes) |
| find.byIcon(Icons.add) | Icons |
| find.widgetWithText(FilledButton, 'Pay') | A button that contains that text |
Keys make tests robust: text may be translated or reworded, but a Key('mobileField') stays the same. Add keys to important inputs and buttons you test often.
Matchers for finders describe how many widgets should match: findsOneWidget, findsNothing, findsWidgets (one or more) and findsNWidgets(3).
pump versus pumpAndSettle
After an interaction, the widget tree does not rebuild by itself in tests. pump() advances one frame (enough after a simple setState). pumpAndSettle() keeps pumping until there are no more scheduled frames, which is what you want after animations or page transitions. Infinite animations (like a spinning progress indicator) never settle, so pumpAndSettle would time out; use pump(const Duration(...)) there.
Widget tests should not call real APIs either. Give the screen a fake repository, or override its provider, so the test controls exactly which data appears and runs the same way every time.
Testing a form
testWidgets('shows an error for a short mobile number', (tester) async {
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: SignupForm())));
await tester.enterText(find.byKey(const Key('mobileField')), '98765');
await tester.tap(find.text('Create account'));
await tester.pump();
expect(find.text('Enter 10 digits'), findsOneWidget);
});Providing state in tests
If the widget reads a Provider, Riverpod provider or Bloc, wrap it the same way in the test, usually with a fake model or repository: ChangeNotifierProvider(create: (_) => FakeCart(), child: MaterialApp(home: CartPage())). With Riverpod, ProviderScope(overrides: [...]) replaces providers with test versions.
Common mistakes
- Forgetting await before tester.tap or pump: the test checks before the UI updates.
- Forgetting pump() after an interaction, then wondering why the text did not change.
- Using pumpAndSettle with an endless animation: timeout.
- Not wrapping in MaterialApp: errors about missing MediaQuery, Directionality or Material ancestors.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Widget tests · 1/6
Widget tests build a widget and interact with it like a user, without a device.
Prefer a video by a teacher? Find free videos on “flutter widget testing testwidgets pumpwidget find tap enterText” on YouTube ↗
In real life: Guarding the checkout button
A grocery app once shipped a bug where the Pay button stayed enabled with an empty cart. The team added a widget test: build CheckoutPage with an empty fake cart and expect the Pay button to be disabled; add an item and expect it enabled with the right total. The bug cannot quietly come back.
testWidgets('Pay is disabled when the cart is empty', (tester) async {
await tester.pumpWidget(MaterialApp(home: CheckoutPage(cart: FakeCart.empty())));
final button = tester.widget<FilledButton>(find.widgetWithText(FilledButton, 'Pay'));
expect(button.onPressed, isNull); // a null onPressed means disabled
});Practice (0/3 done)
After await tester.tap(find.byIcon(Icons.add)), the text still shows the old number. What is missing?
Which matcher checks that an error message is not shown?
Write widget tests for (1) the counter page (initial text, + and - buttons, minus disabled at zero), and (2) a login form: an invalid mobile shows the error, a valid one calls the submit callback once. Add keys to the inputs for robust finders.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Integration tests and CIResponsive design and testing · same module
- Responsive and adaptive designResponsive design and testing · same module
- Interview questions: FlutterFlutter careers and interviews
- PerformanceAnimations and performance
- Implicit animationsAnimations and performance
- Stateless and stateful widgetsFlutter setup and your first app
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which function defines a widget test?
Answer: testWidgets
testWidgets provides a WidgetTester.
What does pumpAndSettle do?
Answer: Pumps frames until no more are scheduled (animations finished)
Use after transitions and animations.
Why add a Key to a field you test often?
Answer: Finders stay stable even if the text changes or is translated
find.byKey is robust.
Do widget tests need a real phone or emulator?
Answer: No, they run in a simulated environment on your computer
That is why they are fast.
What is a widget?Related
Answer: An immutable description of part of the UI
Flutter rebuilds the screen from widgets.
Which binding must integration tests initialise?Related
Answer: IntegrationTestWidgetsFlutterBinding
Call ensureInitialized() first.
Why avoid real network calls in unit tests?Related
Answer: They are slow and flaky and depend on the internet
Replace the API with a fake for fast, reliable tests.
From Unit tests
Which API rebuilds a widget only when one value of the model changes?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Responsive design and testing” before its exam: Integration tests and CI.
- Module 10 exam: Responsive design and testing · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Publishing to the stores and the web. 50 topics left in the course before the final exam.