When to use unit/widget/integration test
Original codelab instructions. My implementation on GitHub.
Learning Objectives
- Unit test for a single piece of the software. An example here is the icon onPressed function (packages:
test
,flutter_test
) - Widget test for one screen. (same packages, but
testWidgets class and WidgetTester class
) - Integration test for entire UI and app performance (packages:
integration_test
,flutter_driver
)
Overall, the unit test designs the test from the perspective of the code; the widget test designs the test from the perspective of possible interactions with a single widget; Integration tests always run on a specific target device as a whole.
Step 1: Set Up The App
The example app lists items you can add or remove from your favorite list.
Favorites model: A ChangeNotifier
(items model) for the favorite items. It maintains a list of items using add() and remove().
Favorites screen: UI for the favorite items. It is a Scaffold
(a screen) that uses Consumer
to update ListView.builder()
made of stateless ListTile
(an item). The trailing of each ListTile
has an IconButton
that lets you remove()
this item using Provider
and ScaffoldMessenger
(as the name implies, it is an InheritedWidget
sits on top of Scaffold
in the tree so that your SnackBar
can live through screens). This makes sense because, to keep this app simple, the only action you want to take on an item that’s already in the favorite list is to remove it. You can’t edit or duplicate. Key
is used to make the items updated correctly (due to how the element tree identifies the changed widget tree, well explained here)
Items screen: UI for all items. Similar to the favorite screen. This is a more generalized case in the sense it has one more action than the the favorite screen besides remove()
: add()
. It again uses ListView
and ListTile
. Another difference is now the icon has two states to indicate whether this icon is added to the favorite list. If it’s added, the icon should be Icons.favorite
. Otherwise, it’s Icons.favorite_border
. You can enter the favorite screen from this item screen through the icon in appBar
. That’s all the difference!
Main: It handles the routing and theme for the entire app. A router is created using GoRouter
with paths defined by the two screens created above. The state of this router is managed by ChangeNotifierProvider
as a MaterialApp.router
child.
Step 2: Unit Test
By convention, the directory structure in the test
directory mimics that in the lib
directory, and the Dart files have the same name with _test
appended.
- Each unit
test()
takes in two arguments, a description and a callback. Inside each test, we call the functions to be tested (add()
andremove()
here), then setexpect()
. - We can organize similar tests into a
group()
the flutter testing framework provides. - You should see something like
00:01 +2: All tests passed!
when cliflutter test test/models/favorites_test.dart
orflutter test
Step 3: Widget Test
- Similar to unit test, widget test takes in the same two arguments, a description, and a callback. However, the difference (1) we use
testWidget()
instead oftest()
(2) the callback takes in aWidgetTester
as its argument to simulate the user behavior, which is stricter but more advanced thantest()
. - Instead of testing the function directly as we did in a unit test, we first need to load this widget in the widget test. We use
tester.pumpWidget()
to load this widget intotestWidgets()
.tester
is theWidgetTester
type as mentioned above. Notice that we don’t just load this widget itself. Instead, we need first to wrap it inChangeNotifierProvider()
andMaterialApp()
because our widget needs to get some data from them by inheriting them in the tree. - Test if ListView rendered: we load the widget,
find.byType(ListView)
, andexpect()
tofindsOneWidget
. - Test Scrolling: For the actual interactions, we use
fling()
andpumpAndSettle()
to simulate the fling gesture and remove all frames. - Test icons: similar to the above test, we load (
pumpWidget
) => expect (byIcon
) => interact (tap
) => expect(text
) again, or any user interaction path you can think of
Step 4: UI Integration Test
The integration test doesn’t follow the unit test file structure convention. It loads in the entire app rather than a single widget to test.
The integration_test
library is used to perform integration tests in Flutter. This is Flutter's version of Selenium WebDriver, Protractor, Espresso, or Earl Gray. The package uses flutter_driver
internally to drive the test on a device.
Since we use ListView, we need to provide a key to uniquely identify a specific widget. We test adding multiple items to the favorite list and then removing multiple items from the favorite list. This is directly done on the entire App()
rather than a createHomeScreen()
as the last testing level.
Step 5: App Performance Test
Performance test also loads the entire app. Hence it’s also an integration test. However, it has extra steps the UI integration test doesn’t have. integration_test
package enables self-driving testing of Flutter code on devices and emulators and adapts flutter_test
results into a format compatible with flutter drive and native Android instrumentation testing.
Note integration_test 1.0.2+3
(previously e2e) is deprecated because it’s moved into Flutter SDK, so rather than installing it yourself, you should find it in your pubspec.yaml
when you flutter create my_app
.
dev_dependencies:
integration_test:
sdk: flutter
You can use FlutterDriver to write the script and run the test. For example, you can take a screenshot of the UI using await binding.takeScreenshot(‘screenshot-1’)
then save it in integrationDriver(onScreenshot)
Now, run the test with
flutter drive \
--driver=test_driver/perf_driver.dart \
--target=integration_test/perf_test.dart \
--profile \
--no-dds
And watch the phone plays with itself.
Isn’t that cool!!!
If you like this tutorial, you may also find my writing on how to build A Voice Bot Mobile App helpful. They are part of my Become Flutter Comfortable in 23 Days series