Testing Flutter Apps on Real Android Devices with Flutter Driver

  December 06, 2019

Since its initial release, Flutter has quickly gained its popularity among developers for building beautiful Android and iOS applications. Like apps built with any other development toolkit, automated testing of Flutter apps is the only way to ensure app quality in the shortest time possible.

In this article, I’d like to talk about how to create the unit, widget and integration tests for automating the testing of Flutter apps and execute them against real Android devices in Bitbar Cloud.

Creating a Sample Bitbar App with Flutter SDK

To better understand how to automate Flutter app testing, I started creating a Bitbar sample app using Flutter SDK (see UI below).

flutter sample app open
flutter sample app open

The MainPage looks like this:

  • Text element
  • 3 button elements (RaisedButton)
  • TextField element
  •  Image asset element

The SubPage looks like this:

  • 2 Text elements
  • button element (RaisedButton)
  • TextField element
  • Image asset element

In my opinion, the easiest way to create a new Flutter app is to use the flutter create command, for example: flutter create my_app. This will create a sample app for Android and iOS.

I created the sample app by modifying this sample app. The app source is in a file called main.dart, and it is in the lib directory.

Note: I gave all the important UI elements Key values, for example:

key: Key('question-text')

Creating Unit and Widget Tests

A ‘unit test‘ is to test a single method or class and a ‘widget test‘ is to test a single widget. Here, a ‘widget‘ means UI elements like layout, button, text box, etc.

Unit tests require a test package (https://pub.dev/packages/test), and the flutter_test package provides additional tools for widget testing.

1. Add the test or flutter_test dependency

You can use the following approach to include test or flutter_test (or both) dependency on the app’s pubspec.yaml file:

dev_dependencies:
  flutter_test:
    sdk: flutter
  test: any
2. Create a unit test file

Create a test directory and test file inside that directory. It is also a good idea to make separate directories for unit and widget tests.

In this example, we can create a file ending with _test.dart for example main_test.dart.

Import packages:

import 'package:test/test.dart';
import 'package:my_app/main.dart';

Sample test:

test('correct answer', () {
  final mainPage = MainPage();
  changeText(true);
  expect(subPageAnswerText, correctAnswerText);
});
3. Create a widget test file

Again create a file ending with _test.dart.

Import packages:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';

Sample test:

testWidgets('correct answer test', (WidgetTester tester) async {
  final app = App();
  await tester.pumpWidget(app);
  expect(find.text("What is the best way to test your applications against over one hundred devices?"), findsOneWidget);
  await tester.tap(find.byKey(Key('correct-answer')));
  await tester.pumpAndSettle();
  expect(find.text("You are right!!!"), findsOneWidget);
});
4. Use command to run the tests

Unit tests can be run with the command:

flutter test test/unit/main_test.dart

And widget tests can be executed with the command:

flutter test test/widget/main_test.dart
5. JUnit Report

One thing to note: If the unit and widget tests are executed in Bitbar Cloud within a CI tool e.g. Jenkins, the test results will not display correctly after the tests are finished.

To get over this, we can use a Flutter package to convert the test results to the JUnit XML report format. (https://pub.dev/packages/junitreport).

First, make sure the following stuff is included in the system path:

  • flutter/.pub-cache/bin
  • flutter/bin/cache/dart-sdk/bin

Then, install the junitreport package by running the command below:

flutter pub global activate junitreport

Both unit and widget tests can be run with this command:

flutter test --machine | tojunit > TEST-all.xml

After the tests are finished, the unit and widget test results can be found in a file called TEST-all.xml.

Creating Integration Tests

An integration test tests the complete app and is isolated from the app under test. Integration tests require a flutter_driver package (https://api.flutter.dev/flutter/flutter_driver/flutter_driver-library.html).

Flutter driver:
  • The application runs in a separate process from the test itself
  • Android (Espresso)
  • iOS (Earl Grey)
  • Web (Selenium WebDriver)
1. Add the flutter_driver dependency

The flutter_driver package needs to be added to dev_dependencies section of the app’s pubspec.yaml file:

dev_dependencies:
  flutter_driver:
    sdk: flutter
2. Create an integration test file

Create a directory called test_driver. Add files main.dart and main_test.dart (or something ending with _test) inside that directory.

Main.dart

This first contains an ‘instrumented’ version of the app being tested. It enables Flutter driver extensions and runs the app.

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

This file is created to contain the test suite.

Import Flutter Driver API:

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

Connect to the app in “setUpAll” method:

setUpAll(() async {
  driver = await FlutterDriver.connect();
});

Close connection in “tearDownAll” method:

tearDownAll(() async {
  if (driver != null) {
    driver.close();
}
});
3. Write a sample integration test:
test('correct answer', () async {

  final goBackButtonFinder = find.byValueKey('back-button');
  final correctAnswerButton1Finder = find.byValueKey('correct-answer');
  final answerTextFinder = find.byValueKey('answer-text');
  String correctAnswerText = "You are right!!!";

  Health health = await driver.checkHealth();
  print(health.status);

  SerializableFinder goBackButton = find.text('Go back');
  try {
    await driver.waitFor(goBackButton);
    await driver.tap(goBackButtonFinder);
  } catch (e) {
    print('try again not visible');
  }

  await driver.tap(correctAnswerButton1Finder);
  expect(await driver.getText(answerTextFinder), correctAnswerText);
});
4. Use command to run the integration tests

Now that we have an instrumented app and a test suite, we can run integration tests with the following command:

flutter driver --target test_driver/main_test.dart

There doesn’t seem to be a ready package for converting integration test results in a format that could be read in a CI tool, at least at the time of writing this. Test results must be parsed and converted to Junit XML format or something else.

5. Take screenshots

Screenshots can be taken in the integration tests with the ‘screenshots’ package (https://pub.dev/packages/screenshots).

Add the dependency in the pubspec.yaml file (the current version of the package at the time of writing this blog):

dev_dependencies:
  screenshots: 2.1.1

Create the screenshots.yaml file inside the project root directory. It should look something like this:

tests:
# Note: flutter driver expects a pair of files eg, main1.dart and main1_test.dart
  - test_driver/main.dart

# Interim location of screenshots from tests
staging: /tmp/screenshots

# A list of locales supported by the app
locales:
  - en-US

devices:
  ios:
    iPad 4 A1459 10.2:
      frame: false
  android:
    Samsung:
      frame: false

# Frame screenshots
frame: false

Add these in the main_test.dart file:

Import dependency:

import 'package:screenshots/screenshots.dart';

Create a config:

final config = Config();

Take screenshot inside test method like this:

await screenshot(driver, config, 'correct-answer');

Testing Flutter Android Apps on Real Devices in Bitbar Cloud

In Bitbar Real Device Cloud, Flutter tests are executed under the Appium Server Side type test project. The ‘run-tests.sh’ shell script is used to run the Flutter tests. Integration, Unit and Widget tests can be run in the same project in the cloud.

Note that only integration tests actually install and run the test in a device, unit and widget tests don’t require devices to run. Device time is still spent when running unit or widget tests, the device is just idling while the test is running.

See a quick demo below.

flutter app testing demo

Unit and Widget tests

Install JUnit report:

flutter pub global activate junitreport
tojunit --help

Run unit and widget tests:

flutter test --machine | tojunit > TEST-all.xml

Move test results to root directory so that they can be found by Jenkins:

cd ..
mv my_app/TEST-all.xml TEST-all.xml

Below is the content of the ‘run-tests.sh’ file

#!/bin/bash

# run-tests file for flutter unit and widget tests
flutter --version
# see what Android SDK is installed
echo $ANDROID_HOME
android list target

echo "Extracting tests.zip..."
unzip tests.zip

# add flutter to path
FLUTTER_PATH="/opt/testdroid/flutter/bin"
PUB_CACHE_BIN="/opt/testdroid/flutter/.pub-cache/bin"
DART_PATH="/opt/testdroid/flutter/bin/cache/dart-sdk/bin"
export PATH=$PATH:$FLUTTER_PATH:$PUB_CACHE_BIN:$DART_PATH

# install Android SDK platform 28 (this can be removed when cloud VMs contains correct version)
echo "install android SDK"
wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
unzip sdk-tools-linux-4333796.zip
# rename old tools and move the new one to its place
mv $ANDROID_HOME/tools $ANDROID_HOME/tools-old
mv tools $ANDROID_HOME/
yes | /root/android-sdk-linux/tools/bin/sdkmanager "platforms;android-28" "build-tools;28.0.3" "platform-tools"

echo "run flutter doctor"
flutter doctor
cd my_app

echo "install junit report"
flutter pub global activate junitreport
tojunit --help

# (build app) get rid of: FormatException: Unexpected character (at character 1) error
flutter test test/unit/main_test.dart

# run unit + widget test and convert to junit format
echo "run unit and widget tests"
flutter test --machine | tojunit > TEST-all.xml

cd ..
mv my_app/TEST-all.xml TEST-all.xml
Integration tests

Run tests with the command:

flutter drive --target=test_driver/main.dart > testconsole.log

Parse results and convert them into a Junit .xml file called ‘TEST-all.xml’ or just look at the log file (console.log) after the test run has ended. Move screenshots to the directory in the root called ‘screenshots’:

cd ..
mkdir -p screenshots
mv /tmp/screenshots/test/ screenshots

How to create and start a Flutter test run in Bitbar Cloud

1. Create a zip-file containing app directory (app and tests, in my case ‘my_app’ directory) and ‘run-tests.sh’ file.

2. Select Android as your target OS type and select the ‘Appium Android Server Side’ type as the framework.

3. Upload your test files to the ‘Appium Server Side’ type project in Bitbar Cloud.

Note: Since the actual Flutter app to test is written in the ‘flutter-tests.zip’ file and will be built during the test run, you could upload any dummy .apk file, e.g. ‘bitbar-sample-app.apk’ (see below) to get the test run started on Bitbar Cloud.

4. Start your Flutter test and get the test results

flutter app testing on bitbar cloud
flutter app testing device session

Conclusion

Testing Flutter apps in Bitbar Cloud is available on real Android devices. Integration test results are not showing correctly without parsing and formatting them yourself. Note that examples in this article have nothing to do with the Appium test automation framework.