How to write your Flutter Integration tests using Patrol.

codedigga

Shalom A.

Posted on October 4, 2024

How to write your Flutter Integration tests using Patrol.

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));
  });
}
Enter fullscreen mode Exit fullscreen mode

Let us consider what the code is doing.

  1. To write our test, we make use of the patrolTest method which is imported from the patrol package. The description parameter describes the test we are writing and the async function parameter runs our code.
  2. 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 our patrolTest function.
  3. 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.
  4. 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),
              ),
Enter fullscreen mode Exit fullscreen mode
  1. 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 here

  2. To 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();
    }
Enter fullscreen mode Exit fullscreen mode

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

  1. 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 .

  2. 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));
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
codedigga
Shalom A.

Posted on October 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related