We all know that no app is maintainable without tests. Indeed, strength of a framework is a function of how easy integration tests are to write and maintain. And I don’t say this lightly – I wrote Android integration tests for a 4.3 rated app with more than one million downloads before Espresso came along!
Flutter offers 3 types of tests: unit, widget, and integration tests. For this code tutorial, I will focus on integration tests – usually the most difficult to write, yet the most rewarding to have.
How it works
Set up
The set up is a bit unusual, in that you need 2 files: one to set up the instrumented app, one for the test. Flutter has adopted a naming convention of descriptionoftest.dart for the first, and descriptionoftest_test.dart for the second. They both must be located in test_driver folder.
To run a test, you use the command line. From your flutter project, run the following command:
1 |
flutter drive --target=test_driver/descriptionoftest.dart |
It doesn’t seem like a setup that will scale well to large apps, but Flutter is only in alpha, so this may not be the final setup.
How to write tests
Tests are driven by Flutter Driver. The application runs in a separate process from the test itself, and Flutter Driver works in a similar manner to Selenium WebDriver.
At the start of the test, you connect to the Driver, and at the end, you disconnect (though the API calls this close rather than disconnect).
The driver performs actions on widgets. The ones we will use in this code tutorial are tap, wait, and waitForAbsent, but there are more, so check the official doc for the full list.
The 2 main ways to find widgets are by key and by text, so keys are very important! In Flutter, keys may be global or local. Check the official doc for more information on the topic. In a way, finding a widget by key is similar to finding a view by its id in Android Espresso, but keys are quite flexible because they are set up in dart (as everything in Flutter) and can therefore be set dynamically (particularly useful for lists).
Gotcha!
The example projects do not have many integration tests and it took me a while to figure out why my test was failing, with the error (or a variation of it) below:
1 |
The built-in library 'dart:ui' is not available on the stand-alone VM. |
It turned out, you get this error if you import Dart files using the flutter library from your test file. I was doing this because my widget key was defined as a constant in the widget dart file. To get rid of this issue, I put all the string constants used for my widget keys in a separate dart file with no flutter import.
This is a serious problem IMHO because it encourages bad coding behaviour (ie copy and paste strings from the app to the test). Again, the framework is only in alpha, so they may well improve on this.
Code example for integration test
For this code tutorial, we will create a simple app where tapping a button in one widget will amend the content in another widget. Then, we will write an integration test to verify this behaviour.
To follow the code tutorial, create a new app as follows.
1 |
flutter create integrationtestexample |
If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.
Code for app
The app is going to use 3 widgets. The first one is for the whole screen. The second one consists of 2 buttons. One button says “SHOW STORES” and the other says “SHOW PRODUCTS”. The third one displays a list of items (either stores or products), or shows a message saying “PLEASE SELECT AN OPTION ABOVE”.
Let’s start with the code in main.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import 'package:flutter/material.dart'; import 'list/list_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'IntegrationTest Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new ListPage(), ); } } |
Now, let’s create a package list and in it, 3 files, one for each widget.
The first is list_page.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
import 'package:flutter/material.dart'; import 'select_list_view.dart'; import 'list_view.dart'; import '../key_strings.dart'; class ListPage extends StatefulWidget { ListPage({Key key}) : super(key: key); @override _ListPageState createState() => new _ListPageState(); } class _ListPageState extends State<ListPage> { List<String> _stores; List<String> _products; List<String> _selectedItems; String _selectedType; @override void initState() { super.initState(); _selectedItems = null; _selectedType = ''; // TODO - this is shortcut to specify products and stores. // In practice, you should load this from a data repository. _stores = new List<String>(); _stores.add('London'); _stores.add('Paris'); _stores.add('Atlanta'); _products = new List<String>(); _products.add('Laptop'); _products.add('Monitor'); } @override Widget build(BuildContext context) { Widget buttonsWidget = new SelectListView(showProducts: _showProducts, showStores: _showStores); Widget itemsWidget = new ItemsListView(typeId: _selectedType, items: _selectedItems); return new Scaffold( appBar: new AppBar( title: new Text("List of items"), ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 4.0), child: new Column(children: <Widget>[ buttonsWidget, new Expanded( child: itemsWidget, ), ], ), ), ); } void _showProducts() { setState(() { _selectedItems = _products; _selectedType = PRODUCT_TYPE; }); } void _showStores() { setState(() { _selectedItems = _stores; _selectedType = STORE_TYPE; }); } } |
For simplification, the list of stores and products is defined as a simple list of string and is hardcoded.
The second is select_list_view.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import 'package:flutter/material.dart'; import '../display_strings.dart'; class SelectListView extends StatelessWidget { SelectListView({Key key, this.showProducts, this.showStores}) : super(key: key); Function showProducts; Function showStores; @override Widget build(BuildContext context) { return new Container( child: new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ new FlatButton( textColor: Colors.blueGrey, color: Colors.white, child: new Text(SHOW_STORES), onPressed: showStores, ), new FlatButton( textColor: Colors.blueGrey, color: Colors.white, child: new Text(SHOW_PRODUCTS), onPressed: showProducts, ), ], ), ); } } |
And the third is list_view.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import 'package:flutter/material.dart'; import '../key_strings.dart'; import '../display_strings.dart'; class ItemsListView extends StatelessWidget { ItemsListView({Key key, this.items, this.typeId}) : super(key: key); List<String> items; String typeId; @override Widget build(BuildContext context) { if (items != null) { return new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: items.map((String value) { return _singleItemDisplay(value, items.indexOf(value)); }).toList()); } else { return new Text(EMPTY); } } Widget _singleItemDisplay(String item, int index) { return new Container( key: new Key(getStringKeyForListItem(typeId, index)), height: 40.0, child: new Container ( padding: const EdgeInsets.all(2.0), color: new Color(0x33000000), child: new Text(item), ), ); } } |
Note the imports for key_strings.dart and display_strings.dart. We define the strings for display and the strings for the widget keys in those files, so we can then use them in the tests.
So let’s create those files (in the main lib package), starting with key_strings.dart.
1 2 3 4 5 6 |
const String PRODUCT_TYPE = 'PRODUCT_'; const String STORE_TYPE = 'STORE_'; String getStringKeyForListItem(String type, int index) { return type + index.toString(); } |
Then display_strings.dart.
1 2 3 |
const String SHOW_STORES = 'SHOW STORES'; const String SHOW_PRODUCTS = 'SHOW PRODUCTS'; const String EMPTY = 'PLEASE SELECT AN OPTION ABOVE'; |
Note that those files have no import for the Flutter framework. Additionally, it is good to separate the display strings, so you can then easily localise to other languages.
Depending on your use cases, it is sometimes better to use keys and other times to use display strings for your tests. Generally, keys are better when you have dynamic content, and display strings are better when you have static content.
Code for test
Let’s move on to the tests now. First, we need to add flutter driver to pubspec.yaml.
1 2 3 4 5 |
dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter |
Then run flutter packages get (or click on “Packages get” in IntelliJ).
Now, we create a test_driver top level folder (ie same level as lib) and a new file list_content.dart in it. We will use it to launch the instrumented version of the app.
1 2 3 4 5 6 7 |
import 'package:flutter_driver/driver_extension.dart'; import 'package:integrationtestexample/main.dart' as app; void main() { enableFlutterDriverExtension(); app.main(); } |
Then, we create the file list_content_test.dart in the same folder, and define our first test. In this test, we verify that the empty list view is showing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import 'package:integrationtestexample/display_strings.dart'; void main() { group('list content test', () { FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('Verify empty list message is shown', () async { SerializableFinder emptyMessage = find.text(EMPTY); await driver.waitFor(emptyMessage); }); }); } |
Let’s run this test to verify the test set up is working. Make sure you have either a device or emulator connected. Then, in the root folder of your project, type the following command flutter drive --target=test_driver/list_content.dart
The output should look similar to this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Using device HTC One M8. Starting application: test_driver/list_content.dart Initializing gradle... 0.4s Resolving dependencies... 0.6s Installing build/app/outputs/apk/app.apk... 11.1s Running 'gradlew assembleDebug'... 5.9s Built build/app/outputs/apk/app-debug.apk (21.5MB). Installing build/app/outputs/apk/app.apk... 6.0s I/flutter ( 714): Diagnostic server listening on http://127.0.0.1:43061/ I/flutter ( 714): Observatory listening on http://127.0.0.1:34552/ 00:00 +0: list content test (setUpAll) [info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:8104/ [trace] FlutterDriver: Looking for the isolate [trace] FlutterDriver: Isolate is paused at start. [trace] FlutterDriver: Attempting to resume isolate [trace] FlutterDriver: Waiting for service extension [info ] FlutterDriver: Connected to Flutter application. 00:02 +0: list content test Verify empty list message is shown 00:02 +1: list content test (tearDownAll) 00:03 +1: All tests passed! Stopping application instance. |
Then we add another test, to verify that tapping on the first button shows the list of stores. We will check that the first store in the list is visible, the first product in the list is not visible, and the empty message is not visible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import 'finders.dart'; void main() { group('list content test', () { FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('Verify empty list message is shown', () async { await driver.waitFor(emptyMessage); }); test('Tap show stores button, verify stores shown', () async { await driver.tap(showStores); await driver.waitFor(firstStore); await driver.waitForAbsent(firstProduct); await driver.waitForAbsent(emptyMessage); }); }); } |
Note how we have moved SerializedFinders to a separate file, to avoid code duplication between test. So create finders.dart in test_driver folder, and add the code below.
1 2 3 4 5 6 7 8 9 10 11 |
import 'package:integrationtestexample/display_strings.dart'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:integrationtestexample/key_strings.dart'; SerializableFinder emptyMessage = find.text(EMPTY); SerializableFinder showStores = find.text(SHOW_STORES); SerializableFinder showProducts = find.text(SHOW_PRODUCTS); SerializableFinder firstStore = find.byValueKey(getStringKeyForListItem(STORE_TYPE, 0)); SerializableFinder firstProduct = find.byValueKey(getStringKeyForListItem(PRODUCT_TYPE, 0)); |
Then we add another test, to verify that tapping on the second button shows the list of products.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import 'finders.dart'; void main() { group('list content test', () { FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) driver.close(); }); test('Verify empty list message is shown', () async { await driver.waitFor(emptyMessage); }); test('Tap show stores button, verify stores shown', () async { await driver.tap(showStores); await driver.waitFor(firstStore); await driver.waitForAbsent(firstProduct); await driver.waitForAbsent(emptyMessage); }); test('Tap show products button, verify products shown', () async { await driver.tap(showProducts); await driver.waitFor(firstProduct); await driver.waitForAbsent(firstStore); await driver.waitForAbsent(emptyMessage); }); }); } |
Running the testS
Now, let’s go to the command line, in your app project, and let’s run the 3 tests.
1 |
flutter drive --target=test_driver/list_content.dart |
The output should look something like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Using device HTC One M8. Starting application: test_driver/list_content.dart Initializing gradle... 0.4s Resolving dependencies... 0.6s Installing build/app/outputs/apk/app.apk... 8.0s Running 'gradlew assembleDebug'... 1.1s Built build/app/outputs/apk/app-debug.apk (21.5MB). I/flutter ( 8102): Diagnostic server listening on http://127.0.0.1:49063/ I/flutter ( 8102): Observatory listening on http://127.0.0.1:52393/ 00:00 +0: list content test (setUpAll) [info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:8114/ [trace] FlutterDriver: Looking for the isolate [trace] FlutterDriver: Isolate is paused at start. [trace] FlutterDriver: Attempting to resume isolate [trace] FlutterDriver: Waiting for service extension [info ] FlutterDriver: Connected to Flutter application. 00:02 +0: list content test Verify empty list message is shown 00:02 +1: list content test Tap show stores button, verify stores shown 00:03 +2: list content test Tap show products button, verify products shown 00:04 +3: list content test (tearDownAll) 00:04 +3: All tests passed! Stopping application instance. |
What next?
Writing tests is something best learnt by doing, so if you haven’t created any Flutter app yet, why not write a test for one of the example apps that come with the framework?
Also, stay tuned, as I plan to write further posts about integration testing on Flutter (as integration testing is a topic I deeply care about!).

Mobile app developer with 12 years experience. Started with the Android SDK in 2010 and switched to Flutter in 2017. Past apps range from start ups to large tech (Google) and non tech (British Airways) companies, in various sectors (transport, commercial operations, e-commerce).
This was very useful, especially the ‘Gotchas’ section. Thanks.
Do you have an example on how to write integration tests in Android using Robolectric?
Robolectric is for use with the Android SDK and not with Flutter, so no, I do not cover its use on my blog, as I currently focus on Flutter. I have never used Robolectric to write tests with the Android SDk anyway, I have always used Espresso https://developer.android.com/training/testing/espresso/.
Thank you for the article. It was really helpful!
I’m running into an issue where the test will hang indefinitely when using a compute (dart isolate). Have you ever run into that? Do you have suggestions?
Thanks again!
Hi!
Not sure if you’ve already seen this but I’m having the same problems with compute operations hanging the execution. This is specifically tricky since the compute is part of the AssetBundle.loadString.
https://github.com/flutter/flutter/issues/24703
Hello! Your tutorial is very useful!)
But I have a problem with getting Text from elements/ It does not work with container:
SerializableFinder firstProduct = find.byValueKey(getStringKeyForListItem(PRODUCT_TYPE, 0));
String firstP = await driver.getText(firstProduct);
print(‘first product is: $firstP’);
Hi, Thank you for the article. Is there any better format for handling the integration test reports? Analyzing the console report might not be feasible when we are running tests in batch.
Hi ,
How do I execute multiple testcases from different files in one go. Like I want to execute Login and homepage test using one command. How should I proceed
-testdriver
–UI_Automation
—loginPage_test.dart
—homepage_test.dart
you can run them using shell script, mentioning the order the test to be run. Something like this,
#!/bin/bash
set -xe
echo “Removing previous run reports….”
flutter driver –target=test_driver/e2e.dart –driver= testdriver/UI_Automation /loginPage_test.dart
flutter driver –target=test_driver/e2e.dart –driver= testdriver/UI_Automation /homepage_test.dart
Please write the next part. How to test other widgets on other screens. How to have multiple test files? How to test Navigation?
This post was the best of the topic of integration that I fount. Please write more!
Thank you for this one too however
How to write an integration test for snackbar(that displays just for few seconds and then dismiss) using flutterdriver.Thanks in Advance.