Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

18. Widget Testing
Written by Alejandro Ulate Fallas

Widget testing is about making your Flutter widgets dance to your tune. It’s essential to ensure your UI components not only look good but also work as you intended. In this chapter, you’ll:

  • Learn the concept of widget testing.
  • Load mock data into the widget tests.
  • Ensure that each ingredient displays correctly.
  • Understand what golden tests are.
  • Add golden tests to verify your widget’s look and feel.

Learning About Widget Tests

Widget testing plays an important role in ensuring your widgets’ reliability and proper functioning. Unlike other testing approaches, widget tests are specifically designed to concentrate on the interaction and underlying logic of the UI elements.

In essence, they act as “unit” tests for your widgets, providing a targeted examination of their behavior, responsiveness and functionality in isolation. This focus helps you identify and address issues early in the development process, contributing to a more robust and error-resistant Flutter application.

Some common scenarios you might want to use widget tests for are:

  • Successful Widget Building: Widget tests are particularly handy for ensuring your widgets build successfully under expected conditions.
  • User Interaction Verification: These tests enable you to simulate user actions, such as tapping or inputting text, ensuring that your widgets respond as expected.
  • State Changes and UI Updates: These tests can also help you confirm that state changes within your widgets work as intended and that these changes are reflected in the user interface.
  • Navigation and Routing Logic: By simulating navigation events, you can verify that your app transitions between screens correctly and that the UI adapts as expected.

Widget tests improve the reliability of your app. They validate critical parts of it, such as building widgets, user interaction, state management, and navigation logic.

It’s time you start working on your own widget tests. Start by adding a new test file that matches the following path test/ui/widgets/ingredient_card_test.dart. You’ll need to create the corresponding directories too.

Then, just so our test suite doesn’t fail, add the following code to your test file:

void main() {}

Use your IDE to run your tests. The result should match the screenshot below:

Adding Your First Widget Test

As you recall, a good scenario for you to get your hands on testing is to verify that the widget builds successfully. This way, you can ensure that your widget’s structure matches your expectations.

Before creating the test, this is a quick reminder of how the widget you’ll test looks:

It’s important to point out that IngredientCard can have multiple variations depending on:

  • evenRow: changes the border and background color of the card depending on its value.
  • showCheckbox: hides or shows the checkbox at the right end of the card.
  • initiallyChecked: marks the checkbox at the right end of the card on the initial build of the widget.
  • If it’s checked, the name displays as striked-through, otherwise, it’s just plain text.

These differences can all produce different results, and they can even combine, ending in more variations. These results are important because they change what you can expect of the widget rendering.

If you’ve got widgets like IngredientCard, it’s smart to test them with different situations. Testing with various combinations means you’re checking how your widget behaves in different situations.

This helps ensure your widget and all its possible versions work the way they should and stay safe from unexpected changes that might pop up during development.

So, here’s the scenario you’ll use to verify that IngredientCard builds properly:

Given IngredientCard is in an evenRow, the checkbox is showing as unchecked when the widget builds, then it should be displayed without issues.

Start by adding the following imports at the top of your file:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:recipes/ui/widgets/ingredient_card.dart';

This imports Flutter’s material library along with the testing toolkit. It also imports IngredientCard, which you’ll be testing.

Then, copy the following code inside main():

testWidgets('IngredientCard can build', (WidgetTester tester) async {
  // TODO: Arrange
  // TODO: Act
  // TODO: Assert
});

testWidgets() is a special function provided by flutter_test. It allows you to build a widget and verify its behaviors. You could build a simple Text or a complex widget with a Scaffold or even build a complete MaterialApp.

You can think of testWidgets() as an equivalent for test() in unit testing.

testWidgets() receives a WidgetTesterCallback. This callback gives you access to a WidgetTester instance (tester in the code above). It allows you to programmatically interact with widgets and the test environment.

Also, you’ll be using the same testing technique as you did for unit testing in the previous chapter: Arrange, Act, Assert. This gives you an organized way to test behaviors and also a repeatable process for all your tests.

Next, replace // TODO: Arrange with this code:

// 1.
const mockIngredientName = 'colby jack cheese';
await tester.pumpWidget(
  // 2.
  MaterialApp(
    home: Scaffold(
      body: ListView(
        children: [
          // 3.
          IngredientCard(
            name: mockIngredientName,
            initiallyChecked: false,
            evenRow: true,
            onChecked: (isChecked) {},
          ),
        ],
      ),
    ),
  ),
);

In detail, here’s what the code above is doing:

  1. First, pumpWidget() renders the UI from the given widget.
  2. You’ve supplied a MaterialApp and other wrappers around IngredientCard since it has some dependencies around theming and context, which is why this is necessary.
  3. Matches the test scenario you were given. showCheckbox is true by default so there’s no need to explicitly declare it when building the widget.

Now, for the Act part of the test, use the following code and replace // TODO: Act:

final cardFinder = find.byType(IngredientCard);
final titleFinder = find.text(mockIngredientName);

find() is a helper function that allows you to search through the widget tree for specific elements and returns all the nodes that match the criteria. cardFinder is an example of how to find a certain widget by using the type class. On the other hand, titleFinder looks for a certain text anywhere in the current widget tree.

Finally, replace // TODO: Assert with the following:

expect(cardFinder, findsOneWidget);
expect(titleFinder, findsOneWidget);

With the code above, you are asserting that both finders can find widgets according to the criteria set. findsOneWidget looks for exactly one widget in the widget tree that matches the criteria.

flutter_test has other assertions already built-in that can help you create finders depending on your case. Here’s a quick look at some of them:

  • findsNothing, when you want the finder to not find anything.
  • findsWidgets, when you want the finder to find one or more widgets.
  • findsNWidgets, when you want the finder to find a specific number of widgets.
  • findsAtLeastNWidgets, when you want the finder to find at least a specific number of widgets.

Use the IDE to run your tests for IngredientCard. They should be passing like in the image below:

Great job! You’ve just added your first widget test.

Testing IngredientCard’s Behaviors

Widget testing becomes super useful when you want to check how your widgets respond to users. You can use these tests to pretend to be a user, clicking buttons or entering information. This way, you make sure your widgets react the right way and give users a smooth experience.

For example, IngredientCard can be checked or unchecked when the user taps it. This is a great scenario to test for since it’ll verify how your widget behaves when the user interacts with it.

This is the next test you’ll add:

Given IngredientCard is in an evenRow, unchecked, and the checkbox is showing, when the user taps on it, then onChecked() should be called with the new value.

But before diving into the test, you’ll reorganize the test file so that you don’t repeat yourself in your tests.

Change the contents of ingredient_card_test.dart for the following:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:recipes/ui/widgets/ingredient_card.dart';

// 1.
Widget _buildWrappedWidget(Widget child) {
  return MaterialApp(
    home: Scaffold(
      body: ListView(
        children: [
          child,
        ],
      ),
    ),
  );
}

void main() {
  // 2.
  const mockIngredientName = 'colby jack cheese';
  group('IngredientCard', () {
    testWidgets('can build', (tester) async {
      // 3.
      await tester.pumpWidget(
        _buildWrappedWidget(IngredientCard(
          name: mockIngredientName,
          initiallyChecked: false,
          evenRow: true,
          onChecked: (isChecked) {},
        )),
      );

      final cardFinder = find.byType(IngredientCard);
      final titleFinder = find.text(mockIngredientName);

      expect(cardFinder, findsOneWidget);
      expect(titleFinder, findsOneWidget);
    });
    // 4.
    testWidgets('can be checked when tapped', (tester) async {
      // TODO: Arrange
      // TODO: Act
      // TODO: Assert
    });
  });
}

Here’s a quick rundown of what you just did:

  1. You’ve added a reusable function to wrap your widgets with a MaterialApp parent tree. This is great because you’re going to need it in all, if not most, of your tests.
  2. Lifted mockIngredientName to be available for all your tests. You’ve also prepared things to group tests together.
  3. Updated your previous test to use the wrapper _buildWrappedWidget.
  4. Added the base for your next test.

Run your tests and ensure that they’re still passing, like in the image below:

Next, replace // TODO: Arrange with this:

var isChecked = false;
await tester.pumpWidget(
  _buildWrappedWidget(IngredientCard(
    name: mockIngredientName,
    initiallyChecked: isChecked,
    evenRow: true,
    onChecked: (newValue) {
      isChecked = newValue;
    },
  )),
);

You’re setting up isChecked which’ll be useful to verify the behavior when the card is tapped. You’ve also set onChecked() to update isChecked when called. That’s about it for the Arrange step.

Now, use the following code to replace // TODO: Act with the code below:

final cardFinder = find.byType(IngredientCard);

await tester.tap(cardFinder);
await tester.pumpAndSettle();

final checkboxFinder = find.byType(Checkbox);

tap() simulates the user tapping the screen of a device. It receives a finder to perform the action on, which in this case is cardFinder. Then, pumpAndSettle() updates the widget tree frame by frame until it settles; hence the name.

Once settled, checkboxFinder is also initialized. You’ll later use it to verify in the next step.

Finally, replace // TODO: Assert with the following:

expect(checkboxFinder, findsOneWidget);
expect(isChecked, isTrue);

With this code, you are checking two things:

  • First, there’s one Checkbox built and visible inside the widget tree.
  • Secondly, that isChecked changed correctly to true after calling tester.tap(cardFinder).

Run your tests again. All tests should pass like the image below:

Your tests are working now! Time to try to verify how the widget looks.

Understanding Golden Tests

While widget testing is great for checking how your widgets work, it might not cover everything about how they look. Widget tests focus more on how things function, not so much on the visual details.

In situations where the exact appearance of a widget is crucial, just relying on widget tests might not be enough.

That’s where golden tests come in. They specifically look at how your widgets appear. A golden test is a type of test that checks whether the visual output of a widget matches an expected ‘golden’ image.

The term ‘golden’ refers to the fact that you have a baseline image (the golden image) that represents the correct appearance of the widget under normal conditions.

If the test suite detects differences between the golden image and the actual widget’s UI, then it’ll fail the test.

IngredientCard Widget Golden Image = IngredientCard Widget Golden Image = Test Failed! Test Passed! Golden Tests

This makes these types of tests particularly useful when working on UI components because they help catch unintended changes in the visual appearance.

If you intentionally change the UI, you might need to update the golden image to reflect the new expected output. This helps prevent unintentional visual regressions.

The flutter_test package already has the built-in features to support golden tests. However, using them on your own might require a complex setup that is hard to replicate from project to project.

This is why golden_toolkit exists. It contains APIs and utilities that build upon Flutter’s Golden test functionality in flutter_test to provide powerful UI regression tests in a simpler way.

To add golden tests to your Flutter project, start by adding golden_toolkit to your pubspec.yml like the following:

golden_toolkit: ^0.15.0

Remember to run flutter pub get afterward to update your dependencies.

Next, create a new file at the root of your project. Name it dart_test.yaml and put the following code inside it:

tags:
  golden:

This indicates that goldens are an expected test tag. All tests that use testGoldens() will get this tag automatically. It also allows you to run golden tests from the command-line.

Open a terminal and run the following command:

flutter test --update-goldens

The output should match something like the following image:

Using --update-goldens updates all the golden images in your tests. You should use this flag sparingly, as it’ll take your tests a little longer to run.

You are now ready to start writing your own golden tests. You’ll be working on that in the next section.

Writing a Golden Test

It’s time to dive into the process of creating your first golden test. IngredientCard supports the light theme very well, and going forward, it’s a good idea to test that it always stays that way.

Open ingredient_card_test.dart again and add the following code after your first test group:

group('Golden Tests - IngredientCard', () {
  testGoldens('can support light theme', (tester) async {
    // TODO: Arrange
    // TODO: Act
    // TODO: Assert
  });
});

testGoldens() is a special function provided by golden_toolkit. It allows you to build a widget and compare it with the golden image. Import golden_toolkit.

import 'package:golden_toolkit/golden_toolkit.dart';

Next, replace // TODO: Arrange with the code below:

final builder = GoldenBuilder.grid(columns: 2, widthToHeightRatio: 1)
// TODO: Scenario for Light - Unchecked
// TODO: Other scenarios

GoldenBuilder builds a column/grid layout for its children. It’ll output a PNG file with a grid layout in the test’s directory.

This builder is needed to compare it to the actual widget you’re testing. For now, what’s important is that you know that GoldenBuilder requires scenarios to build.

A scenario is a specific configuration of your widget that you want to save for later validation. With IngredientCard, you’ll work on adding four different scenarios:

  1. IngredientCard in an even row that displays the checkbox as unchecked in a light theme.
  2. IngredientCard in an even row that displays the checkbox as checked in a light theme.
  3. IngredientCard in an odd row that displays the checkbox as unchecked in a light theme.
  4. IngredientCard in an odd row that displays the checkbox as checked in a light theme.

Now it’s time to add such scenarios. Replace // TODO: Scenario for Light - Unchecked with the following code:

// Scenario 1
..addScenario(
  'Light - Unchecked',
  IngredientCard(
    name: mockIngredientName,
    initiallyChecked: false,
    evenRow: true,
    onChecked: (newValue) {},
  ),
)

Calling addScenario() includes the test scenario into the builder that you initialized before.

Next, replace // TODO: Other scenarios with the code below:

// Scenario 2
..addScenario(
  'Light - Checked',
  IngredientCard(
    name: mockIngredientName,
    initiallyChecked: true,
    evenRow: true,
    onChecked: (newValue) {},
  ),
)
// Scenario 3
..addScenario(
  'Light - Odd - Unchecked',
  IngredientCard(
    name: mockIngredientName,
    initiallyChecked: false,
    evenRow: false,
    onChecked: (newValue) {},
  ),
)
// Scenario 4
..addScenario(
  'Light - Odd - Checked',
  IngredientCard(
    name: mockIngredientName,
    initiallyChecked: true,
    evenRow: false,
    onChecked: (newValue) {},
  ),
);

In the code above, you’re also including the other three scenarios discussed before. The main changes are in how you set up the IngredientCard. That’s pretty much it!

Then, for the next step, replace // TODO: Act with the next code:

// 1.
await tester.pumpWidgetBuilder(
  // 2.
  builder.build(),
  // 3.
  wrapper: materialAppWrapper(
    theme: ThemeData.light(),
  ),
);

Here’s an explanation of the code you just added:

  1. pumpWidgetBuilder() is conveniently included in golden_toolkit to simplify building your widget and golden images.
  2. builder.build() builds the list of scenarios with the layout you set up during the Arrange step.
  3. pumpWidgetBuilder() also allows you to provide a custom wrapper for all your scenarios. This ensures you don’t repeat yourself while adding each scenario and also allows you to customize the configuration for your widget.

Finally, replace // TODO: Assert with the code below:

await screenMatchesGolden(tester, 'light_ingredient_card');

screenMatchesGolden() wraps the API of flutter_test with some extra functionality. This is where golden_toolkit simplifies a lot of the setup needed to compare with golden images.

The second parameter of screenMatchesGolden() is the name of the golden file that’ll be generated. This file will be stored in the current directory of the test in a new folder called goldens/<name-you-specify>.png.

Run your tests again with the flag to update your golden images using the CLI command: flutter test --update-goldens.

The output should match the following image:

Take a look at your test’s directory, too. The test suite generated the golden images used for testing.

If you open that file, it should look something like the image below:

Just to make sure that your golden tests are working properly, you’ll put it to the test. Hypothetically, let’s assume a teammate of yours worked on IngredientCard and accidentally introduced a bug while coding.

Open lib/ui/widgets/ingredient_card.dart. Then, change line 62 to the following code:

value: true,

Now, run your tests for IngredientCard using your IDE. The result should be like in the image below.

Great! Not only did your widget tests catch the behavior, but your golden test also indicates that the changes break how the widget looks, and you were able to catch it before releasing the app to customers!

Before continuing, return IngredientCard to the bug-free version by undoing the change in line 62.

Challenges

Challenge 1: Test IngredientCard Can Be Unchecked

You’ve already added a test to verify the opposite behavior. Use it as an example to test that IngredientCard can be unchecked when tapped and if it was initially checked when rendering the widget.

Here’s a list of the general steps you’ll need to complete this challenge:

  1. Initialize isChecked to true.
  2. Use pumpWidget() to render the UI for your widget.
  3. Set isChecked to the newValue inside IngredientCard’s onChecked() callback.
  4. Find and tap the IngredientCard.
  5. Find Checkbox.
  6. Perform your assertions.

Challenge 2: Test IngredientCard Supports Dark Theme

Again, you’ve already tested that IngredientCard supports the light theme. Now it’s time to check that it also supports the dark theme.

Here’s an overview of the steps you’ll need to complete this challenge:

  1. Use GoldenBuilder to build a grid with two columns.
  2. Add two scenarios, one for checked and another one for unchecked states.
  3. Call pumpWidgetBuilder() to render your widget’s using materialAppWrapper as wrapper.
  4. Set materialAppWrapper theme to Theme.dark().
  5. Assert that your screen matches the golden file with screenMatchesGolden.
  6. Update your golden tests using --update-goldens when testing.
  7. Run your tests and verify they all pass.

Key Points

  • Tools like the flutter_test package provide utilities to make testing easier.
  • Widget tests are great for verifying behaviors.
  • You can verify that a widget builds correctly with a widget test.
  • WidgetTester allows you to perform multiple interactions with your widgets, like tap or even text input.
  • Golden tests are bound to a ‘golden’ image.
  • ‘Golden’ refers to the fact that you have a baseline image that represents the correct appearance of a widget under normal conditions.
  • You can catch unexpected changes to your UI with golden tests.

Where to Go From Here?

You could do so much more with widget testing, so be sure to take a look at Testing in Flutter if you want to learn about testing in much more detail.

There’s also a guided tutorial about Widget Testing in Flutter that can also help you dive further into the great world of testing with Flutter.

Take a look at the guide for Integration Testing that the Flutter team compiled and has published for all Flutter devs.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.