Integration Testing & E2E Flows
Testing complete user journeys on real devices — the final layer of confidence
Open interactive version (quiz + challenge)Real-world analogy
Integration tests are the dress rehearsal before opening night. Unit tests checked each actor knows their lines. Widget tests verified the scenes work. Integration tests run the full show — costume, props, stage, real audience reactions — on the actual theatre.
What is it?
Integration tests (via the integration_test package) run the full Flutter application on a real device or emulator, simulating complete user journeys end-to-end. They provide the highest confidence that the app works as a whole but are the slowest and most expensive tests to maintain.
Real-world relevance
On an offline-first field survey app, an integration test for the sync flow (complete survey offline → go online → verify data synced to server) caught that the Hive database was not flushed before the sync was triggered, causing data loss on low-end Android devices.
Key points
- integration_test package — The official Flutter package for running tests on real devices or emulators. Tests look like widget tests but run against the full app with real plugins, real navigation, and real platform APIs.
- How integration tests differ — They import your real main.dart, run the full app, and interact with it like a user would. Network calls are real (or you use a mock server). Platform channels run for real. This is the highest-fidelity test type.
- Setting up integration_test — Add integration_test to dev_dependencies. Create integration_test/ directory at project root. Tests call IntegrationTestWidgetsFlutterBinding.ensureInitialized() before main(). Run with flutter test integration_test/.
- Testing full user flows — A good integration test covers: app launch → login → perform key action → verify result → logout. On a fintech app this is: launch → BankID login → submit claim → verify confirmation screen → logout.
- Handling async and real time — Use pumpAndSettle() generously but be careful — if animations run forever (TickerMode) or streams never close, pumpAndSettle() will timeout. Use pump(Duration) for known-duration operations.
- patrol package — Patrol extends integration_test with native UI interaction — tapping system dialogs (permissions, notifications), interacting with the notification bar, and using a more ergonomic API via $. It is the production standard for E2E testing.
- Screenshot testing in CI — Use takeScreenshot() (patrol) or tester.takeScreenshot() to capture UI at key points. Store screenshots as artifacts in CI. Useful for visual regression detection across releases.
- CI integration for integration tests — Run on physical device farms (Firebase Test Lab, BrowserStack) or emulators in CI. Typically run less frequently than unit/widget tests — nightly or on release branches — because they are slow (5–15 minutes per run).
- Mocking vs real backend in E2E — Prefer a real staging backend for integration tests. If not available, use a mock HTTP server (mockoon, json-server) pointed to by a test flavor's base URL. Never run E2E tests against production.
- What integration tests CANNOT replace — Integration tests are slow and brittle — small UI changes break locators. They are NOT a substitute for unit tests. Their value is in catching integration bugs: plugin incompatibilities, navigation race conditions, platform-specific rendering.
Code example
// 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();
group('Login to Dashboard E2E Flow', () {
testWidgets('user can log in and reach dashboard', (tester) async {
app.main();
await tester.pumpAndSettle();
// Verify we are on the login screen
expect(find.byKey(const Key('login-screen')), findsOneWidget);
// Enter credentials
await tester.enterText(
find.byKey(const Key('email-field')),
'test@example.com',
);
await tester.enterText(
find.byKey(const Key('password-field')),
'TestPass123!',
);
await tester.pump();
// Tap login
await tester.tap(find.byKey(const Key('login-submit-btn')));
await tester.pumpAndSettle(const Duration(seconds: 5));
// Verify navigation to dashboard
expect(find.byKey(const Key('dashboard-screen')), findsOneWidget);
expect(find.text('Welcome, Test User'), findsOneWidget);
});
testWidgets('user sees error on invalid credentials', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const Key('email-field')),
'wrong@example.com',
);
await tester.enterText(
find.byKey(const Key('password-field')),
'wrongpassword',
);
await tester.tap(find.byKey(const Key('login-submit-btn')));
await tester.pumpAndSettle(const Duration(seconds: 5));
expect(find.text('Invalid credentials'), findsOneWidget);
expect(find.byKey(const Key('login-screen')), findsOneWidget);
});
});
}
// Run with:
// flutter test integration_test/app_test.dart -d <device-id>
// or on Firebase Test Lab:
// gcloud firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apkLine-by-line walkthrough
- 1. IntegrationTestWidgetsFlutterBinding.ensureInitialized() — connects the test runner to the device's test harness; must be the first call
- 2. app.main() — launches your REAL app, not a test stub; all real plugins, navigation, and state management initialise
- 3. tester.pumpAndSettle() — waits for the splash screen and initial navigation animations to complete
- 4. find.byKey(const Key('login-screen')) — Key-based finders are essential in integration tests; text-based finders break on localisation changes
- 5. tester.enterText(...) and tester.tap(...) — simulate real user input; same API as widget tests but running against a real app
- 6. pumpAndSettle(const Duration(seconds: 5)) — allows up to 5 seconds for a real network call to complete and the navigation animation to settle
- 7. expect(find.byKey(const Key('dashboard-screen')), findsOneWidget) — confirms navigation succeeded
- 8. flutter test integration_test/ -d — runs against a specific connected device or emulator
Spot the bug
void main() {
testWidgets('login flow works', (tester) async {
app.main();
await tester.pump();
await tester.enterText(find.byType(TextField), 'user@test.com');
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.byType(DashboardScreen), findsOneWidget);
});
}Need a hint?
Three issues: missing binding initialisation, timing problem after launch, and timing problem after login.
Show answer
Bug 1: Missing IntegrationTestWidgetsFlutterBinding.ensureInitialized() before main() — the test runner cannot communicate with the app without this. Bug 2: tester.pump() after app.main() is insufficient — the app needs pumpAndSettle() to complete its startup animations and initial navigation. Bug 3: tester.pump() after tapping login does not wait for the real network call — use pumpAndSettle(const Duration(seconds: 10)) to wait for the auth response and navigation animation.
Explain like I'm 5
Imagine testing a vending machine. Unit tests check each circuit board works in isolation. Widget tests check the button panel lights up correctly. Integration tests put money in, press a button, and check a real Coke falls out — on the actual machine, with real power, real money, real Coke. It takes longer but it is the most realistic.
Fun fact
Firebase Test Lab runs your integration tests on physical devices — not emulators — across dozens of real Android and iOS device models simultaneously. Google uses the same infrastructure to test their own apps.
Hands-on challenge
Design (do not necessarily implement) an integration test plan for a feature you have built: identify the 3 most critical user flows, specify what device(s) you would run on, and describe what you would do if pumpAndSettle() timed out.
More resources
- integration_test package (pub.dev)
- Flutter Integration Testing Guide (Flutter Docs)
- patrol package — native UI testing (pub.dev)
- Firebase Test Lab for Flutter (Firebase Docs)