内容简介:Testing is important during your app development. As your product grows, it gets more complex, and performing manual tests becomes more difficult. Having an automated testing environment helps optimize this process.Widget testing is like UI testing: You de
Testing is important during your app development. As your product grows, it gets more complex, and performing manual tests becomes more difficult. Having an automated testing environment helps optimize this process.
Widget testing is like UI testing: You develop the look and feel of your app, ensuring every interaction the user makes produces the expected result.
For this tutorial, you’ll write widget tests for a car app called Drive Me , which lets users view a list of cars, view details about them and select and view specific cars. In the process, you’ll learn how to test that the app properly performs the following functions:
- Loads mock data to the widget tests.
- Injects erroneous mock data for negative tests.
- Ensures that the app presents a list of sorted cars and displays its details.
- Checks that the car selection appears correctly in the list page.
- Ensures that the car details page displays correctly.
Note : Note: This tutorial assumes you have some basic knowledge of Flutter. If you are new to this family, take a look at our Getting Started With Flutter tutorial before proceeding.
Getting Started
To start, download the starter project by clicking the Download Materials button at the top or bottom of the tutorial, then explore the starter project in Visual Studio Code. You can also use Android Studio, but this tutorial uses Visual Studio Code in its examples.
Make sure to run flutter packages get
either at the command line or when prompted by your IDE. This pulls the latest version of the packages, rxdart and get_it, which this project uses.
Build and run the project with flutter run
to familiarize yourself with how the app works.
Exploring the Starter Project
The starter project includes the implementation of the app so you can focus on widget testing. Take a look at the contents in lib to understand how the app works.
The project has four main folders:
- database
- details
- list
- models
database contains one main file called cars_database.dart
, which implements an abstract class called CarsDataProvider
that implements loadCars()
. This method parses the JSON file, which has a list of car data, and returns that data as a CarsList
. The model then holds a list of cars and an error message if an exception occurs.
This project uses BLoC
to pass data between the widgets layer and the data layer.
Note : To learn more about BLoC
, visit Getting Started With BLoC Pattern .
Within the details folder, you’ll find car_details_bloc.dart , which gets data from CarsListBloc
. It passes the data to the CarDetails widget in car_details_page.dart .
Next, open car_details_page.dart . On init
, it retrieves the data via CarDetailsBloc
and presents it on the widget. When users select or deselect items, CarsListBloc
makes the updates.
Next, look at cars_list_bloc.dart in the list folder. This is the data layer of CarsList
. This class loads data from JSON and passes it to the widget list. Thereafter, it sorts the cars alphabetically via alphabetiseItemsByTitleIgnoreCases()
.
When the user selects any car, a separate data stream manages it.
In the models folder, you’ll find one more important file. car.dart is where the implementations of Car
and CarsList
reside.
constants.dart contains most of the strings you’ll use throughout the app. dependency_injector.dart is where the app registers the main data layer classes and injects them via get_it
.
Note : If you want to learn more about get_it
and how dependency injection works, read Unit Testing With Flutter .
Now that you’ve tried the app and understand the implementation details, it’s time to start running some tests.
Before you dive deep into the topic of widget testing with Flutter, take a step back and compare it with unit testing.
Unit Testing vs. Widget Testing
Unit testing is a process where you check for quality, performance or reliability by writing extra code that ensures your app logic works as expected. It tests for logic written in functions and methods. The tests then grow and accumulate to cover an entire class and and subsequently a huge part of the project if not all.
The goal of a widget test is to verify that every widget’s UI looks and behaves as expected. Fundamentally, you perform tests by re-rendering the widgets in code with mock data.
This also tells you that if you modify the logic of the app — for example, you change the login validation of the username from a minimum of six characters to seven — then your unit test and widget test may both fail together.
Tests lock down your app’s features, which helps you to properly plan the app’s design before developing it.
Testing Pyramid
There are three types of tests you can perform with Flutter:
- Unit tests : Used to test a method or class.
- Widget tests : These test a single widget.
- Integration tests : Use these to test the critical flows of the entire app.
So, how many tests will you need? To decide, take a look at the testing pyramid. It summarizes the essential types of tests a Flutter app should have:
Essentially, unit tests should cover most of the app, then widget tests and, lastly, integration tests.
Even when good testing grounds are in place, you shouldn’t omit manual testing.
As you go up the pyramid, the tests get less isolated and more integrated. Writing good unit tests help you build a strong base for your app.
Now that you understand the need for testing, it’s time to dive into the project for this tutorial!
Widget Testing the Car List
Before you start widget testing, head to TODO 3
in test/list/cars_list_bloc_test.dart and take a look at the unit tests implemented in this project. These unit tests ensure that the data structure you provide to the widget is accurate.
Before going into writing the test scripts, it’s good to look at the actual screen you’re testing again. In test/database/mock_car_data_provider.dart , the user has selected the first car — the Hyundai Sonata 2017. After the list loads, the selected car will have a blue background. See the image below:
Your First Test
Now, open cars_list_page_test
and add these lines at their appropriate places marked by the TODO number:
testWidgets( "Cars are displayed with summary details, and selected car is highlighted blue.", (WidgetTester tester) async { // TODO 4: Inject and Load Mock Car Data carsListBloc.injectDataProviderForTest(MockCarDataProvider()); // TODO 5: Load & Sort Mock Data for Verification CarsList cars = await MockCarDataProvider().loadCars(); cars.items.sort(carsListBloc.alphabetiseItemsByTitleIgnoreCases); ... });
Injecting test data
This will inject the test data.
Next, add these lines of code at TODO 6 :
// TODO 6: Load and render Widget await tester.pumpWidget(ListPageWrapper()); await tester.pump(Duration.zero);
Here, pumpWidget
renders and performs a runApp
of a stateless ListPage
widget wrapped in ListPageWrapper
. Then, you call pump
to render the frame without delay. This prepares the widget for testing!
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 I 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
.
Ensuring visibility
First, ensure that the Carslist
is in the view. Add these lines of code:
// TODO 7: Check Cars List's component's existence via key final carListKey = find.byKey(Key(CARS_LIST_KEY)); expect(carListKey, findsOneWidget);
In cars_list_page.dart , you will see that the widget tree identifies ListView
with a key called CARS_LIST_KEY
. findsOneWidget
uses a matcher to locate exactly one such widget.
Next, add this function at TODO 8:
// TODO 8: Create a function to verify list's existence void _verifyAllCarDetails(List<Car> carsList, WidgetTester tester) async { for (var car in carsList) { final carTitleFinder = find.text(car.title); final carPricePerDayFinder = find.text(PRICE_PER_DAY_TEXT.replaceFirst( WILD_STRING, car.pricePerDay.toStringAsFixed(2))); await tester.ensureVisible(carTitleFinder); expect(carTitleFinder, findsOneWidget); await tester.ensureVisible(carPricePerDayFinder); expect(carPricePerDayFinder, findsOneWidget); } }
The mock data displays 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.
Refer to the screenshot of the app at the beginning of this tutorial to get a clearer picture of what this test does. It verifies that the title and the price per day display correctly. This is possible because of a function called ensureVisible
.
Hold Command and hover over ensureVisible
to see its description. This function helps the test scroll through the widget tree until it finds the expected widget.
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.
Call the function you just created to verify the car details:
// TODO 9: Call Verify Car Details function _verifyAllCarDetails(cars.items, tester);
Try running the test now — yay, four tests passed!
Widget Testing the Car List Page with Selection
But hang on, your test isn’t done yet. Remember, when you select a car, it should get a blue background? Next, you’ll test to ensure that happens.
Add these lines of code:
// TODO 10: Select a Car carsListBloc.selectItem(1); // TODO 11: Verify that Car is highlighted in blue WidgetPredicate widgetSelectedPredicate = (Widget widget) => widget is Card && widget.color == Colors.blue.shade200; WidgetPredicate widgetUnselectedPredicate = (Widget widget) => widget is Card && widget.color == Colors.white; expect(find.byWidgetPredicate(widgetSelectedPredicate), findsOneWidget); expect(find.byWidgetPredicate(widgetUnselectedPredicate), findsNWidgets(5));
Here’s what this code does:
- TODO 10 : The widget tester attempts to select Car ID 1.
- TODO 11 : It then creates two predicates: one to verify the selected card has a blue background and one to ensure the unselected card remains white.
Try running the test now. Hurray, your test still passes!
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
From TODO 12 to 14, add these lines of code:
testWidgets('Proper error message is shown when an error occurred', (WidgetTester tester) async { // TODO 12: Inject and Load Error Mock Car Data carsListBloc.injectDataProviderForTest(MockCarDataProviderError()); // TODO 13: Load and render Widget await tester.pumpWidget(ListPageWrapper()); await tester.pump(Duration.zero); // TODO 14: Verify that Error Message is shown final errorFinder = find.text(ERROR_MESSAGE.replaceFirst(WILD_STRING, MOCK_ERROR_MESSAGE)); expect(errorFinder, findsOneWidget); });
Here’s what you’re doing with this code:
- TODO 12–13 : You’ve done this before. The only difference here is that you inject
MockCarDataProviderError
, which contains mock error data. - TODO 14 Verify that the error message displays.
Ready for your fifth test? Run it and yaay!!! The fifth test passed!
Verifying view update
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.
Check out how the app looks:
Make the following changes for TODO 15–20:
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(ListPageWrapper()); await tester.pump(Duration.zero); // TODO 17: Verify that Error Message is shown final errorFinder = find.text(ERROR_MESSAGE.replaceFirst(WILD_STRING, MOCK_ERROR_MESSAGE)); final retryButtonFinder = find.text(RETRY_BUTTON); 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 CarsList cars = await MockCarDataProvider().loadCars(); _verifyAllCarDetails(cars.items, tester); });
Here’s what the code above does:
- TODO 15–17 : These are the same as the last test you did.
- TODO 18 : Injects proper mock data.
- TODO 19 : Reloads the widget.
- TODO 20 : Calls the same function to verify all car details.
Time to run the test. Run it now, and … awesome work! Your sixth test passes!
Widget Testing the Car Details Page for the Deselected Car
Finally, move on to the final widget: the Car Details Page. Look at the page again:
From TODO 21–24 in test/details/car_details_page_test.dart
add these lines:
testWidgets('Unselected Car Details Page should be shown as Unselected', (WidgetTester tester) async { // TODO 21: Inject and Load Mock Car Data carsListBloc.injectDataProviderForTest(MockCarDataProvider()); await carsListBloc.loadItems(); // TODO 22: Load & Sort Mock Data for Verification CarsList cars = await MockCarDataProvider().loadCars(); cars.items.sort(carsListBloc.alphabetiseItemsByTitleIgnoreCases); // TODO 23: Load and render Widget await tester.pumpWidget(DetailsPageSelectedWrapper(2)); // Mercedes-Benz 2017 await tester.pump(Duration.zero); // TODO 24: Verify Car Details final carDetailsKey = find.byKey(Key(CAR_DETAILS_KEY)); expect(carDetailsKey, findsOneWidget); final pageTitleFinder = find.text(cars.items[1].title); // 2nd car in sorted list expect(pageTitleFinder, findsOneWidget); final notSelectedTextFinder = find.text(NOT_SELECTED_TITLE); expect(notSelectedTextFinder, findsOneWidget); final descriptionTextFinder = find.text(cars.items[1].description); expect(descriptionTextFinder, findsOneWidget); final featuresTitleTextFinder = find.text(FEATURES_TITLE); expect(featuresTitleTextFinder, findsOneWidget); var allFeatures = StringBuffer(); cars.items[1].features.forEach((feature) { allFeatures.write('\n' + feature + '\n'); }); final featureTextFinder = find.text(allFeatures.toString()); await tester.ensureVisible(featureTextFinder); expect(featureTextFinder, findsOneWidget); final selectButtonFinder = find.text(SELECT_BUTTON); await tester.ensureVisible(selectButtonFinder); expect(selectButtonFinder, findsOneWidget); });
Here’s what you accomplished with the code above:
- TODO 21–23 : Once again, you inject, load and sort the data, then prepare and pump up the widget.
- TODO 24 : Open car_details_page.dart and you’ll find a widget that’s identified with a key, a page title, a deselected title, a features list and a Select button. This entire code helps you to verify them all!
Run the tests now. Your seventh test passed!
Widget Testing Challenge
Your challenge now is to complete the rest on your own. If you get stuck or want to compare solutions, just click Reveal .
- The selected Car Details Page should show a static Selected text at the top of the page. When viewing a selected car, the details page should be represented correctly.
- When selecting and deselecting a car, the details page should update accordingly.
[spoiler]
Testing Details Page for Selected Cars
TODO 25–32 :
testWidgets('Selected Car Details Page should be shown as Selected', (WidgetTester tester) async { // TODO 25: Inject and Load Mock Car Data carsListBloc.injectDataProviderForTest(MockCarDataProvider()); await carsListBloc.loadItems(); // TODO 26: Load and render Widget await tester .pumpWidget(DetailsPageSelectedWrapper(3)); // Hyundai Sonata 2017 await tester.pump(Duration.zero); // TODO 27: Load Mock Data for Verification CarsList actualCarsList = await MockCarDataProvider().loadCars(); List<Car> actualCars = actualCarsList.items; // TODO 28: First Car is Selected, so Verify that final carDetailsKey = find.byKey(Key(CAR_DETAILS_KEY)); expect(carDetailsKey, findsOneWidget); final pageTitleFinder = find.text(actualCars[2].title); expect(pageTitleFinder, findsOneWidget); final notSelectedTextFinder = find.text(SELECTED_TITLE); expect(notSelectedTextFinder, findsOneWidget); final descriptionTextFinder = find.text(actualCars[2].description); expect(descriptionTextFinder, findsOneWidget); final featuresTitleTextFinder = find.text(FEATURES_TITLE); expect(featuresTitleTextFinder, findsOneWidget); var actualFeaturesStringBuffer = StringBuffer(); actualCars[2].features.forEach((feature) { actualFeaturesStringBuffer.write('\n' + feature + '\n'); }); final featuresTextFinder = find.text(actualFeaturesStringBuffer.toString()); await tester.ensureVisible(featuresTextFinder); expect(featuresTextFinder, findsOneWidget); final selectButtonFinder = find.text(REMOVE_BUTTON); await tester.ensureVisible(selectButtonFinder); expect(selectButtonFinder, findsOneWidget); }); <h4>Test that the Selected Car Updates the Widget</h4> testWidgets('Selecting Car Updates the Widget', (WidgetTester tester) async { // TODO 29: Inject and Load Mock Car Data carsListBloc.injectDataProviderForTest(MockCarDataProvider()); await carsListBloc.loadItems(); // TODO 30: Load & Sort Mock Data for Verification CarsList cars = await MockCarDataProvider().loadCars(); cars.items.sort(carsListBloc.alphabetiseItemsByTitleIgnoreCases); // TODO 31: Load and render Widget for the first car await tester .pumpWidget(DetailsPageSelectedWrapper(2)); // Mercedes-Benz 2017 await tester.pump(Duration.zero); // TODO 32: Tap on Select and Deselect to ensure widget updates final selectButtonFinder = find.text(SELECT_BUTTON); await tester.ensureVisible(selectButtonFinder); await tester.tap(selectButtonFinder); await tester.pump(Duration.zero); final deselectButtonFinder = find.text(REMOVE_BUTTON); await tester.ensureVisible(deselectButtonFinder); await tester.tap(deselectButtonFinder); await tester.pump(Duration.zero); final newSelectButtonFinder = find.text(SELECT_BUTTON); await tester.ensureVisible(newSelectButtonFinder); expect(newSelectButtonFinder, findsOneWidget); });
[/spoiler]
Congratulations! You’re now an official Widget Testing Ambassador , go forth and spread the good news!
Where to Go From Here?
Download the final project by clicking the Download Materials button at the top or bottom of this tutorial.
For your next steps, expand your Flutter testing knowledge by exploring the official UI tests cookbook from the Flutter team.
Then take your testing to the next level by exploring and integrating Mockito to mock live web services and databases.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。