Skip to content

Expert · Responsive design and testing · topic 41 of 50 · 35 min

Widget tests

Explanation

Widget tests build a widget in a test environment and interact with it like a user, without a real device. Use testWidgets with a WidgetTester: pumpWidget(MaterialApp(home: ...)) to show it, find.text, find.byType, find.byKey and find.byIcon to locate widgets, tap and enterText to interact, pump or pumpAndSettle to let the UI rebuild and animations finish, and matchers like findsOneWidget and findsNothing.

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.

dart
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);
  });
}
Wrap the widget in MaterialApp (and any providers it needs) so themes, directions and navigation work.

Finders

FinderFinds
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

dart
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);
});
enterText types into a field; validation errors appear after the rebuild.

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.

Think of it like this: A widget test is like a rehearsal on a film set: the scene (widget) is set up, an actor follows a script (tap, type), and the director checks that each line (text, error message) appears at the right moment, all without a real audience or cinema (device).

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

testWidgets

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.

dart
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
});
tester.widget gives you the widget object to inspect its properties.

Practice (0/3 done)

Task 1 · Question

After await tester.tap(find.byIcon(Icons.add)), the text still shows the old number. What is missing?

Task 2 · Question

Which matcher checks that an error message is not shown?

Task 3 · Practice

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

Question 1

1.Which function defines a widget test?

Question 2

2.What does pumpAndSettle do?

Question 3

3.Why add a Key to a field you test often?

Question 4

4.Do widget tests need a real phone or emulator?

Read more (free)

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.

From Everything is a widget

Which binding must integration tests initialise?Related

Answer: IntegrationTestWidgetsFlutterBinding

Call ensureInitialized() first.

From Integration tests and CI

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

Answer: context.select

select listens to a part.

From Provider and ChangeNotifier

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. 1 more topic in “Responsive design and testing” before its exam: Integration tests and CI.
  2. Module 10 exam: Responsive design and testing · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Publishing to the stores and the web. 50 topics left in the course before the final exam.

← Previous: Unit tests