Install Patrol

Check out our video version of this tutorial on YouTube!

If you want to use Patrol finders in your existing widget or golden tests, go to Using Patrol finders in widget tests.

Setup

  • Install patrol_cli:

      flutter pub global activate patrol_cli
    

    Patrol CLI (command-line interface) is a small program that enables running Patrol UI tests. It is necessary to run UI tests (flutter test won't work! Here's why).

    Make sure to add patrol to your PATH environment variable. It's explained in the README.

  • Verify that installation was successful and your environment is set up properly:

    patrol doctor
    

    Example output:

    Patrol CLI version: 2.3.1+1
    Android:
    • Program adb found in /Users/username/Library/Android/sdk/platform-tools/adb
    • Env var $ANDROID_HOME set to /Users/username/Library/Android/sdk
    iOS / macOS:
    • Program xcodebuild found in /usr/bin/xcodebuild
    • Program ideviceinstaller found in /opt/homebrew/bin/ideviceinstaller
    

    Be sure that for the platform you want to run the test on, all the checks are green.

    Patrol CLI invokes the Flutter CLI for certain commands. To override the command used, pass the --flutter-command argument or set the PATROL_FLUTTER_COMMAND environment variable. This supports FVM (by setting the value to fvm flutter), puro (puro flutter) and potentially other version managers.

  • Add a dependency on the patrol package in the dev_dependencies section of pubspec.yaml. patrol package requires Android SDK version 21 or higher.

    flutter pub add patrol --dev
    
  • Create patrol section in your pubspec.yaml:

    pubspec.yaml
    dependencies:
      # ...
    
    dev_dependencies:
      # ...
    
    patrol:
      app_name: My App
      android:
        package_name: com.example.myapp
      ios:
        bundle_id: com.example.MyApp
      macos:
        bundle_id: com.example.macos.MyApp
    

    In this tutorial, we are using example app, which has package name com.example.myapp on Android, bundle id com.example.MyApp on iOS, com.example.macos.MyApp on macOS and My App name on all platforms. Replace any occurences of those names with proper values.

    If you don't know where to get package_name and bundle_id from, see the FAQ section.

  • Integrate with native side

    The 3 first steps were common across platforms. The rest is platform-specific.

    Psst... Android is a bit easier to set up, so we recommend starting with it!

    • Go to android/app/src/androidTest/java/com/example/myapp/ in your project directory. If there are no such folders, create them. Remember to replace /com/example/myapp/ with the path created by your app's package name.

    • Create a file named MainActivityTest.java and copy there the code below.

      MainActivityTest.java
      package com.example.myapp; // replace "com.example.myapp" with your app's package
      
      import androidx.test.platform.app.InstrumentationRegistry;
      import org.junit.Test;
      import org.junit.runner.RunWith;
      import org.junit.runners.Parameterized;
      import org.junit.runners.Parameterized.Parameters;
      import pl.leancode.patrol.PatrolJUnitRunner;
      
      @RunWith(Parameterized.class)
      public class MainActivityTest {
          @Parameters(name = "{0}")
          public static Object[] testCases() {
              PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
              // replace "MainActivity.class" with "io.flutter.embedding.android.FlutterActivity.class" 
              // if in AndroidManifest.xml in manifest/application/activity you have
              //     android:name="io.flutter.embedding.android.FlutterActivity"
              instrumentation.setUp(MainActivity.class);
              instrumentation.waitForPatrolAppService();
              return instrumentation.listDartTests();
          }
      
          public MainActivityTest(String dartTestName) {
              this.dartTestName = dartTestName;
          }
      
          private final String dartTestName;
      
          @Test
          public void runDartTest() {
              PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
              instrumentation.runDartTest(dartTestName);
          }
      }
      
    • Go to the build.gradle.kts file, located in android/app folder in your project directory.

    • Add these 2 lines to the defaultConfig section:

      android/app/build.gradle.kts
        testInstrumentationRunner = "pl.leancode.patrol.PatrolJUnitRunner"
        testInstrumentationRunnerArguments["clearPackageData"] = "true"
      
    • Add this section to the android section:

      android/app/build.gradle.kts
        testOptions {
          execution = "ANDROIDX_TEST_ORCHESTRATOR"
        }
      
    • Add this line to dependencies section:

      android/app/build.gradle.kts
        androidTestUtil("androidx.test:orchestrator:1.5.1")
      

    Bear in mind that ProGuard can lead to some problems if not well configured, potentially causing issues such as ClassNotFoundExceptions. Keep all the Patrol packages or disable ProGuard in android/app/build.gradle.kts:

    android/app/build.gradle.kts
      ...
      buildTypes {
        getByName("release") {
            ...
        }
        getByName("debug") {
            isMinifyEnabled = false
            isShrinkResources = false
        }
      }
    
  • Create a simple integration test

    Let's create a dummy Flutter integration test that you'll use to verify that Patrol is correctly set up.

    Paste the following code into integration_test/example_test.dart:

    integration_test/example_test.dart
    import 'dart:io';
    
    import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:patrol/patrol.dart';
    
    void main() {
      patrolTest(
        'counter state is the same after going to home and switching apps',
        ($) async {
          // Replace later with your app's main widget
          await $.pumpWidgetAndSettle(
            MaterialApp(
              home: Scaffold(
                appBar: AppBar(title: const Text('app')),
                backgroundColor: Colors.blue,
              ),
            ),
          );
    
          expect($('app'), findsOneWidget);
          if (!Platform.isMacOS) {
            await $.native.pressHome();
          }
        },
      );
    }
    

    It does only 2 things:

    • first, it finds a text app
    • then (on mobile platforms), it exits to home screen

    It's a very simple test, but it's enough to verify that Patrol is correctly set up. To run integration_test/example_test.dart on a connected Android, iOS or macOS device:

    patrol test -t integration_test/example_test.dart
    

    If the setup is successful, you should see a summary like one below.

    Test summary:
    📝 Total: 1
    ✅ Successful: 1
    ❌ Failed: 0
    ⏩ Skipped: 0
    📊 Report: <some path>
    ⏱️  Duration: 4s
    

    If something went wrong, please proceed to the FAQ section which might contain an answer to your issue.

If you are looking for a working example of a Flutter app with Patrol tests, check out the example app in the patrol repository.

Initializing app inside a test

To be able to test your app, you need to initialize it and pump the app's root widget, so it appears on the screen. It's very similar to what is done in main function of your app, but it has some key differences, that can break your tests. Easy way to implement it is to copy main function of your app and then adjust it, so it works with Patrol. Here's what to remove when running app inside a patrol test:

  1. DO NOT call WidgetsFlutterBinding.ensureInitialized().
  2. DO NOT use runApp(). Instead, use $.pumpWidget() (or $.pumpWidgetAndSettle() to wait until the UI is rendered). Pass the same argument which was passed to runApp().
  3. DO NOT modify FlutterError.onError. Sometimes it is done by some monitoring tools (like Crashlytics). Those tools rely on intercepting errors by modifying FlutterError.onError callback and it causes that the test engine can't see any exceptions, thus can't end a test if it fails. One way is to move the code that would be common for both the test and the app into a method and leave the rest in main function of the app, or move whole app initialization to a function and define some arguments to enable or diable parts needed in a specific place.

For an example, look at createApp in common.dart in Patrol repository on GitHub.

Flavors

If your app is using flavors, then you can pass them like so:

patrol test --target integration_test/example_test.dart --flavor development

or you can specify them in pubspec.yaml (recommended):

pubspec.yaml
patrol:
  app_name: My App
  flavor: development
  android:
    package_name: com.example.myapp
  ios:
    bundle_id: com.example.MyApp
    app_name: The Awesome App
  macos:
    bundle_id: com.example.macos.MyApp

FAQ

Android

iOS

If you couldn't find an answer to your question/problem, feel free to ask on Patrol Discord Server.

Going from here

To learn how to write Patrol tests, see finders and native automation sections.