Flutter testing using flutter driver

I am planning to share my experience about testing flutter apps using flutter driver. We will try to use all the capabilities of the flutter driver on a very basic app that is autogenerated as soon as you create new flutter project. Lets assume that you have flutter and vscode set up already.

So lets create a flutter project first and open it in vscode:

flutter create testing_tutorial && vscode testing_tutorial

Now lets compile and run our project (you need to have virtual or physical device available for avd or simply build it for a web browser):

cd testing_tutorial && flutter run

You will see a regular counter app. Click + button several times to see tha app works as expected.

Now lets prepare this app for testing with flutter driver. In VS Code open the pubspec.yaml file and add following to the dev_dependencies:

dev_dependencies:
flutter_driver:
sdk: flutter
test: any

After that you will need to run:flutter pub get

This will fetch all the needed libraries for you project.

Now we need to prepare our app for testing with flutter driver. In VS Code create file in folder test_driver/driver.dart file with the content

import 'package:flutter_driver/driver_extension.dart';
import 'package:testing_tutorial/main.dart' as app;
void main() {
enableFlutterDriverExtension();
app.main();
}

In the test_driver folder of the project lets create a file named driver_test.dart. And populate it with the next content:

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
group('Counter App', () {
final buttonFinder = find.byValueKey('increment');
final counterTextFinder = find.byValueKey('counter');
FlutterDriver driver;

setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async => driver?.close());


test('increments the counter', () async {
await driver.tap(buttonFinder, timeout: Duration(seconds: 10));
expect(await driver.getText(counterTextFinder), "1");
});
});
}

Because in the test above we tap and check our widgets using the byValueKey finders we need to add these keys to the app. Open the lib/main.dart file and edit two widgets like:

Text(
'$_counter',
key: Key('counter'),
style: Theme.of(context).textTheme.headline4,
),
...floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
key: Key('increment'),
tooltip: 'Increment',
child: Icon(Icons.add),
),

Now make sure that your virtual or physical device is running so flutter can install app there and run the testing command:

flutter drive --target=test_driver/driver.dart

This command will compile and run the test_driver/driver.dart app and then will trigger test with the same name as app file name, so in this case test_driver/driver_test.dart

The test should be passing. At this point you did a regular tutorial from a flutter integration testing documentation. But what if you need to test some widget properties within your integration test? For example you want to check that the + button icon is of type Icons.add The documentation does not tell you how to do that but it is possible using the data handler function for the flutter driver extension.

Create new file test_driver/helpers.dart:

import 'package:flutter/material.dart';
import 'package:flutter_test/src/all_elements.dart';
List<T> findWidgets<T extends Widget>() =>
collectAllElementsFrom(WidgetsBinding.instance.renderViewElement,
skipOffstage: false)
.map((e) => e.widget)
.whereType<T>()
.toList();
Future<String> dataHandlerFunction(String payload) async {
switch (payload) {
case "getButtonIcon":
var addButton = findWidgets<FloatingActionButton>().first;
return (addButton.child as Icon).icon.codePoint.toString();
default:
return null;
}
}

Here you created dataHandlerFunction which does whatever action you want with the application based on input.

Also pay attention to the findWidgets function. It is a bit difficult to understand but what it does is finds list of widgets of whatever type you feed to it. It is something I retrieved from the deepest parts of flutter source code :) but that is very handy in everyday life.

Now change your test_driver/driver.dart file to

import 'package:flutter_driver/driver_extension.dart';
import 'helpers.dart';
import 'package:testing_tutorial/main.dart' as app;
void main() {
enableFlutterDriverExtension(handler: dataHandlerFunction);
app.main();
}

In such way you set your flutter driver extension handler function to the function defined by you. This data handler function exists within the app and has full access to app resources. If you find the Icons.add in package:flutter/src/widgets/icon_data.dart package you will see its codePoint value 0xe567

Lets now create a test to verify that our icon codePoint has that value. Edit your test_driver/driver_test.dart file by adding this test:

test('check button icon', () async {
var buttonIcon = await driver.requestData("getButtonIcon");
expect(buttonIcon, equals(0xe567.toString()));
});

If you run this test it will pass. You can change the expected value to see that it fails properly. So now you see that in such way you can get whatever information you want about any widget in your app no matter it is visible or not.

But that not all. We can affect our app in any way as well. For example, lets fast forward our counter to 1 000 000 . You need to add this case to data

    case "increaseBy1000000":
var addButton = findWidgets<FloatingActionButton>().first;
for (var i = 0; i < 1000000; i++) addButton.onPressed();
return null;

And then you can add another test:

    test('increments by 1000000', () async {
await driver.requestData("increaseBy1000000");
expect(await driver.getText(counterTextFinder), "1000001");
});

Try to run it, it will be much faster then using driver.tap() one million times.

Also you can deal with custom widgets, data and properties in such way which will not be possible with basic flutter driver capabilities. So this is something for you to start. I will try to provide other interesting features for testing soon in the next part.