LogoPatrol

Tips and tricks

Running a test many times#

Sometimes, you might want to run a test many times. Patrol makes this easy:

patrol test --target integration_test/my_test.dart --repeat 10

You can also use the one-liners below, but keep in mind that they build and test the app 10 times, while patrol test --repeat 10 builds every test target only once and run it 10 times.

Bash

for i in {1..10}; do patrol test --target integration_test/my_test.dart; done

PowerShell

1..10 | ForEach-Object { patrol test --target integration_test/my_test.dart }

Inspecting native view hierarchy#

It's hard to tap on or enter text into a view you don't know how to refer to. In such situation we recommend doing a native view hierarchy dump and finding the properties of the view you want to act on.

Android

First, perform a native view hierarchy dump using adb:

adb shell uiautomator dump

Then, copy the dump file from the device to your machine:

adb pull /sdcard/window_dump.xml .

iOS

The easiest way to perform the native view hierarchy dumb on iOS is to use the idb tool.

Once you have idb installed, perform a dump:

idb ui describe-all

Avoiding hardcoding credentials in tests#

It's a bad practice to hardcode data such as emails, usernames, and passwords in test code.

await $(#nameTextField).enterText('Bartek'); // bad!
await $(#passwordTextField).enterText('ny4ncat'); // bad as well!

To fix this, we recommend removing the hardcoded credentials from test code and providing them through the environment:

await $(#nameTextField).enterText(const String.fromEnvironment('USERNAME'));
await $(#passwordTextField).enterText(const String.fromEnvironment('PASSWORD'));

Make sure that you're using const here because of issue #55870.

To set USERNAME and PASSWORD, use --dart-define:

patrol test --dart-define 'USERNAME=Bartek' --dart-define 'PASSWORD=ny4ncat'

Alternatively you can create a .patrol.env file in your project's root. Here's an example:

$ cat .patrol.env
EMAIL=user@example.com
PASSWORD=ny4ncat
DISABLE_ANALYTICS=true

Granting sensitive permission through the Settings app#

Some particularly sensitive permissions (such as access to background location or controlling the Do Not Disturb feature) cannot be requested in the permission dialog like most of the common permissions. Instead, you have to ask the user to go to the Settings app and grant your app the permission you need.

Testing such flows is not as simple as simply granting normal permission, but it's totally possible with Patrol.

Below we present you with a snippet that will make the built-in Camera app have access to the Do Not Disturb feature on Android. Let's assume that the Settings app on the device we want to run the tests on looks like this:

settings_screenshot

And here's the code:

await $.native.tap(Selector(text: 'Camera')); // tap on the list tile
await $.native.tap(Selector(text: 'ALLOW')); // handle the confirmation dialog
await $.native.pressBack(); // go back to the app under test

Please note that the UI of the Settings app differs across operating systems, their versions, and OEM flavors (in case of Android). You'll have to handle all edge cases yourself.

Ignoring exceptions#

If an exception is thrown during a test, it is marked as failed. This is Flutter's default behavior and it's usually good – after all, it's better to fix the cause of a problem instead of ignoring it.

That said, sometimes you do have a legitimate reason to ignore an exception. This can be accomplished with the help of the WidgetTester.takeException() method, which returns the last exception that occurred and removes it from the internal list of uncaught exceptions, so that it won't mark the test as failed. To use it, just call it once:

final widgetTester = $.tester;
widgetTester.takeException();

If more than a single exception is thrown during the test and you want to ignore all of them, the below snippet should come in handy:

var exceptionCount = 0;
dynamic exception = $.tester.takeException();
while (exception != null) {
  exceptionCount++;
  exception = $.tester.takeException();
}
if (exceptionCount != 0) {
  $.log('Warning: $exceptionCount exceptions were ignored');
}

Handling permission dialogs before the main app widget is pumped#

Sometimes you might want to manually request permissions in the test before the main app widget is pumped. Let's say that you're using the geolocator package:

final permission = await Geolocator.requestPermission();
final position = await Geolocator.getCurrentPosition();
await $.pumpWidgetAndSettle(MyApp(position: position));

In such case, first call the requestPermission() method, but instead of awaiting it, assign the Future it returns to some final. Then, use Patrol to grant the permissions, and finally, await the Future from the first step:

// 1. request the permission
final permissionRequestFuture = Geolocator.requestPermission();
// 2. grant the permission using Patrol
await $.native.grantPermissionWhenInUse();
// 3. wait for permission being granted
final permissionRequestResult = await permissionRequestFuture;
expect(permissionRequestResult, equals(LocationPermission.whileInUse));
final position = await Geolocator.getCurrentPosition();
await $.pumpWidgetAndSettle(MyApp(position: position));

See also:

Bundling many tests into a single app binary#

A new application binary (.apk for Android and .app for iOS) is built for every Dart test file in the integration_test directory. This is slow and suboptimal, but there's a workaround that makes it possible to bundle many tests into a single application binary.

Let's say that you have 2 tests: add_comment_test.dart and follow_user_test.dart. To bundle these tests:

  1. Create a third file with any name of your choice, e.g bundled_test.dart.

  2. Import and run the 2 tests from bundled_test.dart:

    import 'add_comment_test.dart' as add_comment_test;
    import 'follow_user_test.dart' as follow_user_test;
    
    void main() {
      group('bundled tests', () {
        add_comment_test.main();
        follow_user_test.main();
      });
    }
    

Now you can run the bundled_test.dart:

patrol test --target integration_test/bundled_test.dart

This builds the application binary once, saving you some time.

Bear in mind that this can increase test flakiness since many test run in the single app. Normally, the app in reinstalled and its data is cleared between tests, which prevents many issues.

See also: