Introduction to Flutter UI Test - Part 1
Moataz Nabil
Author of 'Mobile DevOps Playbook' | Software Engineering Manager, Platform Engineering | AWS Community Builder | Developer Advocate
Flutter - A Game Changer for Mobile App Development
In Part 2 I will demonstrate how to run our UI Tests on Continuous Integration servers like Codemagic
What is Flutter?
Flutter is a Google new mobile UI framework which aims to help developers in building native mobile apps for Android as well as iOS platform from a single codebase. The major components of Flutter include:
- Flutter engine: Flutter’s engine, written primarily in C++, provides low-level rendering support using Google’s Skia graphics library.
- Foundation library: The Foundation library, written in Dart, provides a base layer of functionality for apps and APIs to communicate with the engine.
- Widgets: Widgets are the basic building blocks for UI in Flutter. Designing UI for an app involves composing and tweaking various simple widgets like text, shapes, and animations to create more complex ones.
The Engine architecture
The Flutter Engine is a portable runtime for high-quality mobile applications. It implements Flutter's core libraries, including animation and graphics, file and network I/O, accessibility support, plugin architecture, and a Dart runtime and toolchain for developing, compiling, and running Flutter applications.
Why choose Flutter for your app?
Flutter offers various widgets and tools that allow Flutter developers to build stunning applications for iOS and Android with a single code base.
Powered by Dart
Flutter uses the Google-developed Dart language. If you’ve used Java before, you’ll be fairly familiar with the syntax of Dart as they are quite similar. Besides the syntax, Dart is a fairly different language.
Write Once, Run on Android and iOS
Developing mobile apps can take a lot of time considering you need to use a different codebase for Android and iOS.That is unless you use an SDK like Flutter, where you have a single codebase that allows you to build your app for both operating systems. Not only that, but you can run them completely natively
Reactive framework
Flutter uses the reactive framework which makes updating content through UI obsolete. Now easily update content on your Flutter application by updating variables in your state and UI will automatically reflect all the changes.
Hot reload
Flutter offers the functionality of Hot Reload, which enable developers to change the code and check for results directly in the application. Hot Reload enables quicker debugging and faster bug fixing.
Material design and Cupertino
Flutter has completely revolutionized the cross-platform app development by offering a widget library which offers a pleasing UI using Material Design by Google and Cupertino by Apple.
Installing Flutter
I’m using the following machine configuration and software version:
- MacBook Pro running macOS Mojave
- Android Studio 3.4.1
- Xcode 10.2.1
- Flutter 1.5.4
Note: You can use IntelliJ IDEA or Microsoft VS Code and Install the required extension/plugin for Flutter
First up, head over to Flutter Installation page to install Flutter. I will skip the steps here as the steps in the document is detailed enough.
Once you run flutter doctor and you got the following results, you are good to go! It’s not necessary to have Connected Devices checked.
flutter doctor
Doctor summary (to see all details, run flutter doctor -v): [?] Flutter (Channel stable, v1.5.4-hotfix.2, on Mac OS X 10.14.5 18F132, locale en-DE) [?] Android toolchain - develop for Android devices (Android SDK version 28.0.3) [?] iOS toolchain - develop for iOS devices (Xcode 10.2.1) [?] Android Studio (version 3.4) [?] IntelliJ IDEA Community Edition (version 2018.3.4) [?] Connected device (1 available) ? No issues found!
Creating a new Flutter Project
With Flutter installed, now let’s start to build your first Flutter project.
First, fire up Android Studio and click Start a new Flutter Project.
Next, select Flutter Application and click Next.
Then fill in Project name flutter_test_app with anything you like. By default, it should show your default path of the Flutter path. In case it doesn’t work for you, navigate and specify your own Flutter SDK path. Then, click Next.
Finally, fill in a Company domain. This will be replicated in your Bundle Identifier (iOS) & Package Name (Android). For my case, I checked both Kotlin & Swift support. Then, click Finish.
The Project structure
After creating the Flutter app you will find the following project structure with different directories for Android and iOS and also the important file pubspec.yaml to manage the project dependencies
Trying out an App on iOS Simulator
Once you started your Flutter Application, some code is automatically generated with a sample app that allows you to hit a button and perform some text updates. Before we make any code changes, it is a good checkpoint to try running it on your iOS simulator.
To run the app, find the dropdown list somewhere at the top right that says, click on it and select Open iOS Simulator.
Click Run, which is the green triangle, and the app should open in your simulator. You should be able to interact with the Demo app and push a few buttons!
Trying out an App on Android Emulator
Stop the previous run and select your emulator from the list and click run again, the build will start a Gradle task assembleDebug to install the application on the device
So we have one single code base for Android and iOS
Writing UI Test for our app using Flutter driver
Flutter has 3 types of tests:
- Unit tests are the one used for testing a method, or class.
- Widget tests are the tests for controlling a single widget.
- Integration tests are tests the large scale or all of the application.
Unit Tests
Unit tests are there to test the logic of your application.e.g. to test a method or class behavior. Mostly they do not interact with user input. When the external dependencies are needed, it can be mocked with packages like Mockito.
Widget Tests
Widget tests are the tests to check if a widget is created as expected and also to check if widget interactions work as expected.
The creation process of the widget tests is similar to unit tests. Extra to unit test, we use the WidgetTester utility that Flutter provides for testing the widgets. With these tests, it is possible to send taps, scroll to a certain position, etc. in the widget.
Integration Testing (We are focusing on this type in our article)
Integration tests are the test types that we give instructions to our application to do something and check if the wanted behavior is happening in the application. The creation process and running the tests are completely different from unit and widget tests.
Before we start writing the UI tests we need to change the code in main.dart file inside lib directory to be like the following :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@overrideWidget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
home: MyHomePage(title: 'Flutter App Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@overrideWidget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
// Provide a Key to this specific Text Widget. This allows us// to identify this specific Widget from inside our test suite and// read the text.
key: Key('counter'),
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
// Provide a Key to this the button. This allows us to find this// specific button and tap it inside the test suite.
key: Key('increment'),
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
For creating an integration test, first of all, we need to have Flutter driver in pubspec.yaml file. You can add it by adding the following code under dev_dependencies.
flutter_driver:
sdk: flutter
test: any
For starting the test creation process, we should create an instrumented version of the application’s start point. For creating that, let’s create a folder called test_driver. Under this folder, let’s create a file called app.dart and add the code below.
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test_app/main.dart' as app;
void main() {
// This line enables the extension
enableFlutterDriverExtension();
// Call the `main()` function of your app or call `runApp` with any widget you// are interested in testing.
app.main();
}
What this code does, enables the Flutter driver extension. This extension enables us to create an integration test. After it’s enabled, we can call our main method to start the application.
Create another file called app_test.dart under the same directory, So the following is the directory structure:
flutter_test_app/
lib/
main.dart
test_driver/
app.dart
app_test.dart
Write the tests
Now that we have an instrumented app, we can write tests for it! This will involve four steps:
- Create SeralizableFinders to locate specific Widgets.
- Connect to the app before our tests run in the setUpAll function.
- Test the important scenarios
- Disconnect from the app in the teardownAll function after our tests complete
Inside the app_test.dart we will write the following code:
// Imports the Flutter Driver API
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('Flutter App', () {
// First, define the Finders. We can use these to locate Widgets from the// test suite. Note: the Strings provided to the `byValueKey` method must// be the same as the Strings we used for the Keys in step 1.
final counterTextFinder = find.byValueKey('counter');
final buttonFinder = find.byValueKey('increment');
FlutterDriver driver;
// Connect to the Flutter driver before running any tests
setUpAll(() async {
driver = await FlutterDriver.connect();
});
// Close the connection to the driver after the tests have completed
tearDownAll(() async {
if (driver != null) {
driver.close();
}
});
test('starts at 0', () async {
// Use the `driver.getText` method to verify the counter starts at 0.
expect(await driver.getText(counterTextFinder), "0");
});
test('increments the counter', () async {
// First, tap on the button 5 timesawait driver.tap(buttonFinder);for (int i = 0; i < 4; i++) {await driver.tap(buttonFinder);
}
// Then, verify the counter text has been incremented by 5
expect(await driver.getText(counterTextFinder), "5");
});
});
}
Now we can run our application, Be sure that you are connected to a device to test. then use the command below to run the integration test from the command line.
flutter drive --target=test_driver/app_test.dart
Note:
If you have 2 connected devices you need to specify the device to run the test or you will get the following message and it will use the Android SDK:
So the command can be like (in my case):
flutter drive --target=test_driver/app.dart -d iPhone X?
flutter drive --target=test_driver/app.dart -d Android SDK built for x86
After the test finished you can check the results in the console
You can find the full code in the following GitHub repository
Resources
Good Luck and Happy Testing :)
Moataz Nabil
Quality Assurance Team Lead/Consultant, ISTQB Certified Advanced Level - Test Manager
5 年?I was wondering if it is possible to create a page object model for testing in flutter with a base class for driver invocation and page objects and tests in other class? I would appreciate any light on that. Thanks You!