Flutter Integration Testing: integration_test Package and Patrol
Flutter integration tests run your actual app on a real device or emulator — including platform channels, camera, location, and real network requests. The integration_test package (part of the Flutter SDK) replaces the deprecated flutter_driver. For advanced scenarios like system dialogs, notifications, and deep links, Patrol provides a higher-level API. This guide covers both.
Key Takeaways
Integration tests need a device or emulator. They can't run on the Dart VM. Connect a device with flutter devices before running.
IntegrationTestWidgetsFlutterBinding.ensureInitialized() must be the first call. Without it, the binding that connects your test to the Flutter runtime isn't set up.
pumpAndSettle() is your friend in integration tests. Real devices have real animations and real async operations. pumpAndSettle() waits for everything to finish before asserting.
Patrol's $ operator is find.text + tester.tap combined. await $.tap(find.text('Login')) is equivalent to multiple WidgetTester calls. It also adds native auto-waiting.
Keep integration tests to critical flows only. Integration tests are slow (30s–5min each). 20 integration tests is plenty for most apps. Cover the happy path of your most important features.
When to Use Integration Tests
Integration tests are expensive — they require a running device, take minutes, and are flakier than widget tests. Use them for:
- Authentication flow (login → authenticated state)
- Core user journeys (purchase, sign-up, onboarding)
- Platform-specific features (camera, location, notifications)
- Deep link handling
- Background/foreground state transitions
For everything else, use widget tests (faster, more isolated) or unit tests (fastest).
Setup
Add to pubspec.yaml:
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutterCreate the integration test directory:
integration_test/
app_test.dartWriting Integration Tests
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // required
group('App integration tests', () {
testWidgets('onboarding flow completes successfully', (tester) async {
app.main();
await tester.pumpAndSettle();
// First onboarding screen
expect(find.text('Welcome to MyApp'), findsOneWidget);
await tester.tap(find.text('Get Started'));
await tester.pumpAndSettle();
// Second screen
expect(find.text('Set up your profile'), findsOneWidget);
await tester.enterText(find.byKey(const Key('name-field')), 'Alice');
await tester.tap(find.text('Continue'));
await tester.pumpAndSettle();
// Final screen
expect(find.text('You\'re all set!'), findsOneWidget);
});
testWidgets('full login and logout cycle', (tester) async {
app.main();
await tester.pumpAndSettle();
// Login
await tester.enterText(
find.byKey(const Key('email-field')),
'alice@example.com',
);
await tester.enterText(
find.byKey(const Key('password-field')),
'password123',
);
await tester.tap(find.byKey(const Key('login-btn')));
await tester.pumpAndSettle();
// Verify logged in
expect(find.text('Dashboard'), findsOneWidget);
expect(find.text('alice@example.com'), findsOneWidget);
// Logout
await tester.tap(find.byKey(const Key('menu-btn')));
await tester.pumpAndSettle();
await tester.tap(find.text('Logout'));
await tester.pumpAndSettle();
// Back on login screen
expect(find.byKey(const Key('login-btn')), findsOneWidget);
});
});
}Running Integration Tests
# Connect a device first
flutter devices
<span class="hljs-comment"># Run on connected device
flutter <span class="hljs-built_in">test integration_test/
<span class="hljs-comment"># Run on specific device
flutter <span class="hljs-built_in">test integration_test/ -d <span class="hljs-string">"iPhone 15 Pro"
<span class="hljs-comment"># Run on emulator
flutter <span class="hljs-built_in">test integration_test/ -d emulator-5554
<span class="hljs-comment"># Run with verbose output
flutter <span class="hljs-built_in">test integration_test/ --verboseHandling Real Network Requests
Integration tests hit real APIs by default. For deterministic tests, either:
- Use a test environment URL
- Mock the HTTP client
// Option 1: Use test environment
// Set up via environment variables or Flutter's --dart-define
const apiUrl = String.fromEnvironment('API_URL', defaultValue: 'https://test-api.myapp.com');
// Option 2: Use a mock HTTP client
import 'package:http/testing.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('loads users from API', (tester) async {
final mockClient = MockClient((request) async {
if (request.url.path == '/api/users') {
return http.Response(
'[{"id":"1","name":"Alice"}]',
200,
headers: {'content-type': 'application/json'},
);
}
return http.Response('Not found', 404);
});
app.main(httpClient: mockClient); // inject mock client
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
});
}Patrol for Advanced Testing
Patrol adds native device interactions that the standard integration_test package can't do:
# Add patrol CLI
dart pub global activate patrol_cli
<span class="hljs-comment"># Add to pubspec.yaml
<span class="hljs-comment"># dependencies:
<span class="hljs-comment"># patrol: ^3.0.0Patrol lets you:
- Handle system dialogs (permission requests, alerts)
- Interact with the notification center
- Test deep links
- Handle app state changes (background/foreground)
import 'package:patrol/patrol.dart';
void main() {
patrolTest(
'app requests location permission on first launch',
($) async {
await $.pumpWidgetAndSettle(const MyApp());
// Trigger location request
await $.tap(find.text('Enable Location'));
// Handle native permission dialog
await $.native.grantPermissionWhenInUse();
// Verify location was enabled
expect(find.text('Location enabled'), findsOneWidget);
},
);
}Running Patrol Tests
# Run patrol tests
patrol <span class="hljs-built_in">test integration_test/
<span class="hljs-comment"># Run with specific device
patrol <span class="hljs-built_in">test integration_test/ --device <span class="hljs-string">"iPhone 15 Pro"CI Integration
For CI, use Firebase Test Lab, AWS Device Farm, or run on an emulator:
# GitHub Actions with Android emulator
jobs:
integration-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
- name: Install dependencies
run: flutter pub get
- name: Start Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
script: flutter test integration_test/For iOS in CI, use a Mac runner with Xcode installed.
Test Organization
For larger apps, split integration tests by feature:
integration_test/
auth/
login_test.dart
logout_test.dart
forgot_password_test.dart
products/
product_list_test.dart
product_detail_test.dart
checkout/
checkout_flow_test.dartRun all:
flutter test integration_test/Run one feature:
flutter test integration_test/auth/Debugging Failing Integration Tests
When an integration test fails, you often can't see what happened. Use screenshots:
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('creates a product', (tester) async {
app.main();
await tester.pumpAndSettle();
await binding.takeScreenshot('01-home-screen');
await tester.tap(find.text('Add Product'));
await tester.pumpAndSettle();
await binding.takeScreenshot('02-add-product-form');
// ... rest of test
});Production Monitoring vs Integration Tests
Integration tests run on-demand against a test environment. They don't monitor your production app continuously.
HelpMeTest runs automated tests against your live backend services 24/7. If your Flutter app's API goes down at 3am, you'll know before your users do. Start with the free tier — 10 tests, no credit card needed.