Widget Testing With Flutter: Getting Started

In this tutorial about Widget Testing with Flutter, you’ll learn how to ensure UI widgets look and behave as expected by writing test code. By Stef Patterson.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Your First Test

Open test/list/cars_list_page_test.dart and add the following beneath // TODO 4: Inject and Load Mock Car Data:

carsListBloc.injectDataProviderForTest(MockCarDataProvider());

This is injecting the mock car test data into carsListBloc.

Beneath // TODO 5: Load & Sort Mock Data for Verification add:

final cars = await MockCarDataProvider().loadCars();
cars.items.sort(carsListBloc.alphabetizeItemsByTitleIgnoreCases);

Here you’re waiting for the mock car data to load and then sort the list.

Now it’s time to inject the test data.

Add these lines of code below // TODO 6: Load and render Widget:

await tester.pumpWidget(const ListPageWrapper());
await tester.pump(Duration.zero);

pumpWidget() renders and performs a runApp of a stateless ListPage widget wrapped in ListPageWrapper(). Then, you call pump() to render the frame and specify how long to wait. In this case you don’t want a delay so Duration.zero is used.

This prepares the widget for testing!

When you have a structure (i.e. list, collections) with repeated data models, pump() becomes essential to trigger a rebuild since the data-loading process will happen post-runApp.

Note: pumpWidget calls runApp, and also triggers a frame to paint the app. This is sufficient if your UI and data are all provided immediately from the app, or you could call them static data (i.e., labels and texts).

When you have a structure (i.e. list, collections) with repeated data models, pump() becomes essential to trigger a rebuild since the data-loading process will happen post-runApp.

Beneath // TODO 7: Check Cars List's component's existence via key to ensure that the Carslist is in the view add these lines of code:

final carListKey = find.byKey(const Key(carsListKey));
expect(carListKey, findsOneWidget);

If you look at lib/list/cars_list_page.dart, you will see that the widget tree identifies ListView() with a key called carsListKey(). findsOneWidget uses a matcher to locate exactly one such widget.

The mock data in mock_car_data_provider.dart has a total of six cars, but you don’t want to write a test for each one. A good practice is to use a for loop to iterate through and verify each car on the list.

Return to test/list/cars_list_page_test.dart and below // TODO 8: Create a function to verify list's existence add this:

void _verifyAllCarDetails(
  List<Car> carsList,
  WidgetTester tester,
) async {
  for (final car in carsList) {
    final carTitleFinder = find.text(car.title);
    final carPricePerDayFinder = find.text(
      pricePerDayText.replaceFirst(
        wildString,
        car.pricePerDay.toStringAsFixed(2),
      ),
    );
    await tester.ensureVisible(carTitleFinder);
    expect(carTitleFinder, findsOneWidget);
    await tester.ensureVisible(carPricePerDayFinder);
    expect(carPricePerDayFinder, findsOneWidget);
  }
}

This test verifies that the title and the price per day display correctly. This is possible because of a function called ensureVisible().

Car List with selected card highlighted in blue

To see more about ensureVisible(), hover over it to see its description automatically displayed.

Popup showing ensureVisible definition

Theoretically, a ListView also contains a scrollable element to allow scrolling. The test doesn’t currently verify images.

Testing images is expensive: It requires getting data from the network and verifying chunks of data. This can lead to a longer test duration as the number of test cases increases.

Note: You wrap a ListView in a SingleChildScrollView to make this work in cars_list_page.dart. At the time of writing, you must do this for the test to pass.

Theoretically, a ListView also contains a scrollable element to allow scrolling. The test doesn’t currently verify images.

Testing images is expensive: It requires getting data from the network and verifying chunks of data. This can lead to a longer test duration as the number of test cases increases.

To verify the car details, find // TODO 9: Call Verify Car Details function and add this below it to call to the function you just created:

_verifyAllCarDetails(cars.items, tester);

In the next section you’ll learn how to add tests to verify the selected car has a blue background.

Widget Testing the Car List Page with Selection

Remember when you select a car it has a blue background? You need to create a test to ensure that happens.

Still in cars_list_page_test.dart, add this beneath // TODO 10: Select a Car:

carsListBloc.selectItem(1);

The widget tester attempts to select Car ID 1.

Find // TODO 11: Verify that Car is highlighted in blue add the following below it:

// 1
bool widgetSelectedPredicate(Widget widget) =>
          widget is Card && widget.color == Colors.blue.shade200;
// 2
bool widgetUnselectedPredicate(Widget widget) =>
          widget is Card && widget.color == Colors.white;

expect(
   find.byWidgetPredicate(widgetSelectedPredicate),
   findsOneWidget,
);
expect(
  find.byWidgetPredicate(widgetUnselectedPredicate),
  findsNWidgets(5),
);

Here you have created two predicates:

  1. Verify the selected card has a blue background
  2. Ensure the unselected card remains white

Run this test now. Hurray, your new test passes! :]

Selected cars background turns blue

You’re doing very well. It’s time to try some negative tests before finishing with the testing of the car details page.

Negative Tests for Car List Page

Now it’s time to test for errors. To simulate errors, add the following below // TODO 12: Inject and Load Error Mock Car Data:

carsListBloc.injectDataProviderForTest(MockCarDataProviderError());

You’ve injected data before. The only difference here is that you inject MockCarDataProviderError(), which contains mock error data.

Below // TODO 13: Load and render Widget add:

  await tester.pumpWidget(const ListPageWrapper());
  await tester.pump(Duration.zero);

As before, pumpWidget() and pump() trigger a frame to paint and render immediately.

Beneath // TODO 14: Verify that Error Message is shown add the following to add error messages.

final errorFinder = find.text(
  errorMessage.replaceFirst(
    errorMessage,
    mockErrorMessage,
  ),
);
expect(errorFinder, findsOneWidget);

This replaces the errorMessage with the mockErrorMessage and confirms the error message displays.

Ready for your fifth test? Run it.

Proper error message displayed

Great job! Your fifth test passed!

There’s one last test you need to perform for this widget, which is to verify the widget updates its view if data comes in after getting an error.

You need to test if your app doesn’t have any cars to display.

Carlist Error Data

Since this next step includes code you’ve already used, you’re going to do a large update at once. Find and replace // TODO Replace testWidgets('''After encountering an error...''' and the entire placeholder testWidgets() beneath it with:

testWidgets(
    '''After encountering an error, and stream is updated, Widget is also 
    updated.''',
    (WidgetTester tester) async {
      // TODO 15: Inject and Load Error Mock Car Data
      carsListBloc.injectDataProviderForTest(MockCarDataProviderError());

      // TODO 16: Load and render Widget
      await tester.pumpWidget(const ListPageWrapper());
      await tester.pump(Duration.zero);

      // TODO 17: Verify that Error Message and Retry Button is shown
      final errorFinder = find.text(
        errorMessage.replaceFirst(
          errorMessage,
          mockErrorMessage,
        ),
      );
      final retryButtonFinder = find.text(retryButton);
      expect(errorFinder, findsOneWidget);
      expect(retryButtonFinder, findsOneWidget);

      // TODO 18: Inject and Load Mock Car Data
      carsListBloc.injectDataProviderForTest(MockCarDataProvider());
      await tester.tap(retryButtonFinder);

      // TODO 19: Reload Widget
      await tester.pump(Duration.zero);

      // TODO 20: Load and Verify Car Data
      final cars = await MockCarDataProvider().loadCars();
      _verifyAllCarDetails(cars.items, tester);
    },
  );

Here’s what the code does:

  • TODO 15–17: These are the same as the tests you did in the last step.
  • TODO 18: Injects car mock data.
  • TODO 19: Reloads the widget.
  • TODO 20: Waits for mock dat to load and then verifies the car details.

Time to run the test. Run it now, and …

Proper error message shown

Awesome work! Your sixth test passes!

You’ve tested for when a car is selected. What about when it’s been deselected? You guessed it, that’s next.