As developers, we write automated tests to enable us always verify if our codes and features work as expected especially when debugging and improving them. This will help us catch bugs introduced early and fix them quickly.
While there are several types of automated tests we can write, in this article, we will be focused on writing Integration tests in Flutter using Patrol.
What is Patrol?
Patrol is an amazing testing framework that allows us write integration tests using simple intuitive syntaxes. An integration test will usually test our app like a normal user would from end-to-end.
Why Patrol?
There is also Flutter's integration_test
package that can help us with writing integration tests but one way Patrol stands out is in allowing us also test the native aspects of our app. These include the native views such as permissions dialogs and webviews which are not easily accessible directly from flutter when writing tests. Also, Patrol provides us simpler and shorter methods to carry out our testing.
To learn the basics of Patrol, we will be writing integration tests for a small application as shown:
You can find the GitHub repo for this tutorial here.
Step 1: Set up patrol.
Before you can begin writing your tests, you will need to add the patrol
package to your project, activate the patrol_cli
on your local system as well as other minor configurations to your project files. Follow the steps outlined in the docs to do this.
Step 2: Add your test files.
Firstly, create a new folder in the root directory of your project (same level as your /lib
folder) and name it integration_test
. You will add your integration tests files here. Our example project on Github has three files in it but to keep this short, we will only focus on the e2e_test.dart
file. Feel free to explore with your own files and tests.
Step 3: Writing the test.
Our app is a simple 2-page application. We will be testing the flow from the user endpoint. To do this, our test will log into the app, deny the app permissions the first time we are prompted, navigate to the webview and back, request permissions again and grant them this time, then finally we sign out.
The test for this is contained in the following code:
// e2e_test.dart
void main() {
patrolTest('full e2e test', ($) async {
// first we initialize our app for test (pumping and settle)
await $.pumpWidgetAndSettle(const MyApp());
await Future<void>.delayed(const Duration(seconds: 2));
// find the email textfield by its key and enter the text `admin`
await $(#email_field).enterText('admin');
// enter password also
await $(#password_field).enterText('admin');
// tap on our sign in button to sign in
await $(#login_btn).tap();
// wait for the permission dialog to become visible then dismiss it by denying permission
if (await $.native.isPermissionDialogVisible()) {
await $.native.denyPermission();
}
await $.pumpAndSettle();
await $(#request_btn).waitUntilVisible(timeout: const Duration(seconds: 3));
await Future<void>.delayed(const Duration(seconds: 2));
// tap on the button to open our webview
await $(#webview_btn).tap();
await Future<void>.delayed(const Duration(seconds: 5));
// dismiss the webview by going back to previous screen
await $.native.pressBack();
await Future<void>.delayed(const Duration(seconds: 2));
// request for permission and grant them when the dialog becomes visible
await $(#request_btn).tap();
if (await $.native.isPermissionDialogVisible()) {
await $.native.grantPermissionWhenInUse();
}
await $.pumpAndSettle();
// if permission was granted, the request button should be removed
expect($(#request_btn), findsNothing);
await Future<void>.delayed(const Duration(seconds: 2));
// we tap on sign out button
await $(#signout_btn).tap();
// if signed out, we should be taken back to the Login page.
expect($('Login').exists, equals(true));
});
}
Let us consider what the code is doing.
- To write our test, we make use of the
patrolTest
method which is imported from thepatrol
package. The description parameter describes the test we are writing and the async function parameter runs our code. - The
main
function is also needed to enable us run our code. This is the function that will be called when the test starts which in turn runs ourpatrolTest
function. - Before we can test our app, we will need to provide it to our test runner. We do this via the
pumpWidgetAndSettle
method. This should be the first step in our tests. - To be able to interact with widgets in our app, we will need to find the exact widget to tap, slide, or carry out any interaction with. In testing, we have the concept of
finders
which are simple utility methods to help us locate widgets. We can find widgets in our app using various properties such as their types (ListTile, ElevatedButton, Text etc.), their content (e.g Text widget with 'Log in') or through keys we assign to the widgets. Using keys should be the preferred way to go as this ensures we can always find the same widget even if the code is modified or more widgets are introduced. Notice how we pass keys to our widgets to help us locate them during test.
TextField(
key: const Key('email_field'), // widget key
controller: _emailController,
decoration: const InputDecoration(
label: Text('Email'),
floatingLabelBehavior: FloatingLabelBehavior.always,
floatingLabelAlignment: FloatingLabelAlignment.start),
),
In Patrol, we make use of the syntax
$(#key)
to find widgets by their keys. To find widgets by type, we can do something like:$(ListTile)
. You can learn more on how to use finders hereTo interact with the native views, we may call the available methods on the object
$.native
. This will be called for both iOS and Android platforms.
// wait for the permission dialog to become visible then dismiss it by denying permission
if (await $.native.isPermissionDialogVisible()) {
await $.native.denyPermission();
}
There are several methods that can be called on this object and a list of the supported methods for each platform can be found here
Take note of the
expect()
method. This is the assertion that checks the behaviour we expect. It confirms that the first argument, which most times will be a finder, matches the second argument which is our .Since the tests can be very fast during execution, we have introduced some delays in the code to lower the speed of execution and make it easy on the eye while running on the simulator/emulator.
await Future<void>.delayed(const Duration(seconds: 2));
Finally, to run the test, we can connect our physical device or start up an emulator/simulator and run the command patrol test -t integration_test/e2e_test.dart
from our terminal.
This should run your test and if everything passed or any test fails, you can find the logs on the terminal and in the outputs folder.
Conclusion
This article was a shallow dive into how awesome Patrol is and how we can now easily write our integration tests in Flutter and even test interactions with native views. To learn more on Patrol, you can always refer to the documentation. I hope you learned something that will help you improve your testing strategy.
Have any questions? Drop them in the comments.
Happy Digging!
Top comments (1)
hi @codedigga
thanks for the tutorial. it's works perfectly on my side.
but i have a question related to
flavor
if the flutter project is using
flavor
, what package name should I put onMainActivityTest.java
file?is it
com.example.flutter_patrol_tutorial
orcom.example.flutter_patrol_tutorial.dev
?