Skip to main content
Debugging is an essential part of test development. This guide covers techniques for debugging Patrol tests and resolving common issues.

Debugging in Visual Studio Code

You can debug your application during Patrol tests by attaching a debugger to the running process.

Setup Instructions

1

Add debugger configuration

In your launch.json file, add a new configuration for attaching debugger to a process:
{
  "name": "attach debugger",
  "request": "attach",
  "type": "dart",
  "cwd": "patrol_test",
  "vmServiceUri": "${command:dart.promptForVmService}"
}
2

Run tests with develop command

Run your patrol tests using develop command with the same arguments as you would normally do:
patrol develop --target patrol_test/app_test.dart
3

Copy the VM Service URI

When the tests start running, you will see a message with a link to Patrol devtools extension. Copy the last part of the URI from the message.For example, for this link:
Patrol DevTools extension is available at http://127.0.0.1:9104/patrol_ext?uri=http://127.0.0.1:52263/F2-CH29gR1k=/
Copy http://127.0.0.1:52263/F2-CH29gR1k=/.
You’ll see 2 similar logs. First one looks like this:
The Dart VM service is listening on http://127.0.0.1:63725/57XmBI_pwSA=/
Ignore it - this link is incorrect, wait for the one that says about devtools extension.
4

Attach the debugger

From “Run and Debug” tab in Visual Studio Code, select the configuration you created in step 1. You will be prompted to enter the VM service URI. Paste the URI you copied in step 3.
5

Set breakpoints and debug

Once the debugger is attached, you can set breakpoints and debug your application as you would normally do.
IntelliJ/Android Studio does not support attaching a debugger to a running process via Observatory Uri. Therefore you cannot achieve the same behavior in those IDEs. See this issue for more details.

Hot Restart for Faster Development

When writing Patrol tests, use the patrol develop command for faster iteration:
patrol develop --target patrol_test/app_test.dart
Anytime you add a new line of test code, just type “r” in the terminal to re-run the tests without the time-costly app building.
This approach is much more time-effective than running patrol test from scratch every time you make a change.

Common Issues and Solutions

Problem: Your finder matches multiple widgets on the screen.Solution: Use the .at(index) method to specify which widget you want:
// If there are multiple TextFormFields
await $(TextFormField).at(0).enterText('first field');
await $(TextFormField).at(1).enterText('second field');
Better Solution: Use unique keys instead:
await $(#emailTextField).enterText('[email protected]');
await $(#passwordTextField).enterText('password');
Problem: grantPermissionWhenInUse() fails because the permission was already granted.Solution: Check if the permission dialog is visible before trying to accept it:
if (await $.platform.mobile.isPermissionDialogVisible()) {
  await $.platform.mobile.grantPermissionWhenInUse();
}
This is especially important when using patrol develop for local development, where app state may persist between runs.
Problem: Test fails with “Widget not found” even though the widget exists.Possible causes:
  1. Widget hasn’t finished rendering
  2. Widget is hidden/scrolled off-screen
  3. Wrong finder selector
Solutions:
// Wait for widget to appear
await $(#myWidget).waitUntilVisible();

// Scroll to widget if it's off-screen
await $(#myWidget).scrollTo();

// Verify your selector is correct
expect($(#myWidget).exists, equals(true));
Problem: Test times out waiting for an action to complete.Solution: Increase the timeout for specific operations:
// For notifications or delayed operations
await $.platform.mobile.tapOnNotificationBySelector(
  Selector(textContains: 'Notification title'),
  timeout: const Duration(seconds: 5),
);

// For widgets that take time to appear
await $(#myWidget).waitUntilVisible(
  timeout: const Duration(seconds: 10),
);
Problem: Tests work on your machine but fail in continuous integration.Common causes:
  1. Different device/emulator configurations
  2. Timing issues (CI machines may be slower)
  3. Environment variable differences
Solutions:
// Use longer timeouts in CI
await $.pumpAndSettle(timeout: const Duration(seconds: 10));

// Ensure credentials are provided via environment
await $(#email).enterText(const String.fromEnvironment('EMAIL'));

// Test on the same device/emulator type as CI

Inspecting Native View Hierarchy

When you need to interact with native views and don’t know how to refer to them, perform a native view hierarchy dump.

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 dump on iOS is to use the idb tool. Once you have idb installed, perform a dump:
idb ui describe-all
Use the dumped hierarchy to find the exact properties (text, resource-id, content-desc) of the native views you want to interact with.

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.

Ignore a Single Exception

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

Ignore Multiple Exceptions

If more than a single exception is thrown during the test and you want to ignore all of them:
var exceptionCount = 0;
dynamic exception = $.tester.takeException();
while (exception != null) {
  exceptionCount++;
  exception = $.tester.takeException();
}
if (exceptionCount != 0) {
  $.log('Warning: $exceptionCount exceptions were ignored');
}
Only ignore exceptions when you have a legitimate reason. Always prefer fixing the underlying issue.

Debugging Tips

Use $.log()

Add logging statements to track test execution:
$.log('Starting login flow');
await $(#loginButton).tap();
$.log('Login button tapped');

Check Widget Existence

Verify widgets exist before interacting:
expect($(#myWidget).exists, equals(true));

Take Screenshots

Capture screenshots at key points:
await $.takeScreenshot('after_login');

Use DevTools Extension

Monitor test execution in real-time with Patrol DevTools extension for deeper insights.

Build docs developers (and LLMs) love