Flutter Integration Testing: A Modern, End‑to‑End Guide
A modern, practical guide to Flutter integration testing: setup, writing reliable tests, running on devices and web, CI examples, and troubleshooting.
Image used for representation purposes only.
Overview
Integration tests exercise your full Flutter app on a real device or emulator, validating end‑to‑end user journeys: launch → interact → verify. They sit above unit and widget tests, catching wiring issues across UI, navigation, networking, storage, and platform integrations. This guide shows you how to set up, write, run, and automate modern integration tests with the integration_test package, plus battle‑tested practices to keep them reliable and fast.
Note: As of March 2026, flutter_driver is deprecated. Use integration_test for all new work and when migrating existing suites.
When to use integration tests
Use them for flows where multiple layers must cooperate correctly:
- Authentication (sign in/out, password reset)
- Onboarding and permissions
- Search and filtering with server data
- Checkout and payments
- Deep links and navigation across screens
- Critical regressions you never want to ship
Keep them focused and few. Favor unit and widget tests for logic and rendering; reserve integration tests for the highest‑value journeys.
Project setup
Add integration_test and flutter_test to dev_dependencies, and create the standard integration_test/ folder.
# pubspec.yaml
name: my_app
# ...
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter_lints: ^3.0.0 # optional but recommended
flutter:
uses-material-design: true
Directory layout:
- lib/
- test/ # unit/widget tests
- integration_test/ # full end‑to‑end tests
Your first test
Create integration_test/app_test.dart:
// integration_test/app_test.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app; // runs your production entrypoint
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
group('Auth flow', () {
testWidgets('signs in with valid credentials', (tester) async {
// Launch the app just like a user would.
app.main();
await tester.pumpAndSettle();
// Interact with the UI using stable Keys.
await tester.enterText(find.byKey(const Key('email_field')), 'user@example.com');
await tester.enterText(find.byKey(const Key('password_field')), 'correct-horse');
await tester.tap(find.byKey(const Key('sign_in_button')));
// Let animations/network settle.
await tester.pumpAndSettle();
// Verify we landed on Home.
expect(find.byKey(const Key('home_screen')), findsOneWidget);
// Optional: capture a screenshot for debugging/visual checks.
await binding.takeScreenshot('home_after_login');
}, timeout: const Timeout(Duration(minutes: 2)));
});
}
Add Keys to your production widgets for robust selection:
// Example: in your login screen
TextField(key: const Key('email_field'), ...)
TextField(key: const Key('password_field'), ...)
ElevatedButton(key: const Key('sign_in_button'), onPressed: _submit, ...)
Running tests locally
- Start an emulator or connect a device.
- Android: use Android Studio or
flutter emulators --launch <id> - iOS: open Simulator from Xcode, or connect a provisioned device
- Web: ensure Chrome is installed and listed under
flutter devices
- Run the suite:
# Run all integration tests on the first available device
flutter test integration_test
# Target a specific device
flutter devices # list device IDs
flutter test integration_test -d emulator-5554
# Run a single file
flutter test integration_test/app_test.dart -d emulator-5554
# Filter by test name (regex)
flutter test integration_test --name "signs in"
# Web (headless is not recommended for flakey UIs; use Chrome)
flutter test integration_test -d chrome --web-renderer html
Tips:
- Build types: tests run using a debug‑like build by default for speed. Validate release builds separately in your pipeline.
- Logs: use
flutter logsin another shell for device logs, andprint()inside tests for quick breadcrumbs.
Structuring a reliable suite
Design for clarity, speed, and isolation.
- One flow per test: keep scenarios focused (login, add to cart, checkout). Split long scripts into discrete tests; they’re easier to triage and retry.
- Deterministic finders: prefer
find.byKeyandfind.bySemanticsLabelover localizable texts. - Synchronization:
await tester.pumpAndSettle()after taps and navigations. Avoid arbitraryFuture.delayedexcept as a last resort. - App restarts: call
app.main()at the start of each test if state leaks across tests. Keep global singletons resettable. - Timeouts: set explicit per‑test timeouts to catch an infinite spinner early.
- Data independence: seed predictable state (e.g., dedicated test users, stable fixtures). Clean up server state at the end or use idempotent APIs.
- Flaky guards: favor backing services with test doubles on lower envs; keep live‑service tests minimal.
Test data and environments
Parameterize your app so tests can point at a staging backend and use special test accounts.
- Dart defines: pass flags to your app’s main via
--dart-defineor--dart-define-from-file.
# Example .env.staging.json
{
"API_BASE_URL": "https://staging.api.example.com",
"FEATURE_FLAGS": "checkoutV2"
}
# Launch tests against staging
flutter test integration_test -d emulator-5554 \
--dart-define-from-file=.env.staging.json
- Authentication: pre‑create disposable accounts. For OTP/passcode, expose a test bypass endpoint or mail sink in staging.
- Permissions: configure emulators with pre‑granted permissions, or drive system dialogs using a helper (e.g., key‑driven taps on platform UI). Avoid real hardware prompts when possible.
Network strategies
Decide how “real” your tests should be:
- Fully online: highest confidence, highest flake risk. Use a stable staging env and idempotent test data.
- Hybrid: rely on real auth and navigation but stub volatile endpoints (e.g., ad calls) using a proxy or a conditional client in test mode.
- Offline: for pure UI flows, consider a test app flavor that swaps the HTTP client with a fixture loader.
A simple switch:
// main.dart
void main() {
const baseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'https://api.example.com');
runApp(MyApp(api: Api(baseUrl: baseUrl)));
}
Screenshots, performance, and reports
- Screenshots: helpful for debugging CI failures or lightweight visual checks.
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await binding.takeScreenshot('cart_after_add');
- Performance traces: wrap actions with tracing to gather frame timings and durations.
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
final timeline = await binding.traceAction(() async {
// do something expensive, e.g., open product list and scroll
await tester.tap(find.byKey(const Key('open_products')));
await tester.pumpAndSettle();
});
// Attach structured data to the test report
binding.reportData = {
'open_products_timeline': timeline.toJson(),
};
- Artifacts: upload screenshots and JSON metrics from your CI for later inspection.
Accessibility and localization checks
- Semantics: prefer
find.bySemanticsLabelfor interactive elements to keep tests resilient and promote accessibility. - Large text/different locales: run the same test matrix on emulators configured with larger font scales and target locales. Keep strings externalized; avoid assertions on localized copy.
Web specifics
- Use
-d chromewith--web-renderer htmlfor broad compatibility; prefercanvaskitfor closer production parity if that’s what you ship. - Headless browsers can mask layout differences; run at least one pass with a visible Chrome.
CI: GitHub Actions examples
Run on Android emulator:
# .github/workflows/integration.yml
name: integration-tests
on: [push, pull_request]
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter --version
- run: flutter pub get
- name: Start emulator and run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
profile: pixel_6
script: flutter test integration_test -d emulator-5554 --name ""
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: screenshots-android
path: build/**/screenshots/**/*.png
Run on iOS simulator (macOS runners only):
ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- name: Boot iOS Simulator
run: |
xcrun simctl bootstatus "iPhone 15" -b || xcrun simctl boot "iPhone 15"
- name: Run tests on iOS Simulator
run: flutter test integration_test -d "iPhone 15"
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: screenshots-ios
path: build/**/screenshots/**/*.png
Web (Chrome):
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- run: flutter pub get
- run: flutter test integration_test -d chrome --web-renderer html
Sharding longer suites:
- Split by file: run multiple jobs each executing a subset of test files.
- Split by tag: organize scenarios with
@Tags(['slow'])and run--tags slow/--exclude-tags slow.
Troubleshooting common issues
- No devices found: ensure an emulator is running (
flutter emulators --launch) or a device is connected and authorized (flutter devices). - Test hangs on spinners: inspect synchronization; add targeted
await tester.pump(const Duration(milliseconds: 100));between animations; verify network responses in staging. - Flaky taps: scroll the target into view (
await tester.ensureVisible(finder)) before tapping; avoid tapping while progress indicators are present. - Permissions dialogs appear unexpectedly: pre‑grant in emulator images or gate the permission request behind a test feature flag.
- Element not found after navigation: add
await tester.pumpAndSettle()afterNavigatorpushes; verify that your Keys are on the final widget, not an ancestor you later replaced.
Migration notes from flutter_driver
- Tests move from an external driver (VM service protocol) to in‑app binding.
- Replace
FlutterDrivercalls withtesterinteractions. - Use
IntegrationTestWidgetsFlutterBinding.ensureInitialized()and run viaflutter test integration_testinstead offlutter drive. - Performance tracing is available via
traceAction; screenshots viatakeScreenshot.
Best practices checklist
- Keep flows short and stable; prefer multiple small tests over a giant script.
- Use Keys and semantic labels. Avoid brittle text matchers.
- Parameterize environments with dart‑defines.
- Seed controlled data; keep tests idempotent.
- Synchronize with
pumpAndSettle; avoid fixed sleeps. - Capture screenshots and logs on failure; upload artifacts.
- Shard in CI; do not run multiple tests concurrently on the same device.
- Review and prune: delete obsolete tests and keep the suite under 10–15 minutes.
Closing thoughts
Integration tests give you confidence that the experience users feel in their hands truly works. With integration_test, you can drive real devices using the familiar flutter_test API, keep scenarios readable, and automate them across Android, iOS, and Web. Start by covering your top three user journeys, wire them into CI, and iterate from there—your release nights (and your users) will thank you.
Related Posts
Flutter Impeller Rendering Engine: A Performance Deep Dive and Tuning Guide
A practical performance deep dive into Flutter’s Impeller renderer: how it works, how to measure it, and tuning patterns for smooth, jank‑free UIs.
Flutter Local Notifications: A Complete Scheduling Guide (Android 13/14+ ready)
A practical Flutter guide to scheduling reliable local notifications on Android and iOS, updated for Android 13/14 exact-alarm and permission changes.
React Native vs Flutter Performance: What Actually Matters
A practical, engineer-focused comparison of React Native vs Flutter performance—from startup time to frame pacing, with tooling and optimization tips.