Integration Tests
4 min read
Testing
/\ Integration tests (few)
/ \ ~10s each — full app, real device, real navigation
/────\
/ \ Widget tests (medium)
/ \ ~ms each — single widget pumped, fake binding
/──────────\
/ \ Unit tests (many)
/ \ ~µs each — pure Dart, no Flutter binding
/────────────────\
| Layer | Lives in | Runs on | Speed | Catches |
|---|---|---|---|---|
| Unit | test/ | Dart VM | Fast | Logic bugs |
| Widget | test/ | Dart + flutter_test binding | Fast | UI behavior, widget composition |
| Integration | integration_test/ | Real device / emulator / web | Slow | End-to-end flow regressions, platform integration |
Code in action — a login flow
// integration_test/login_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 flow', () {
testWidgets('user can log in and reach home', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password123');
await tester.tap(find.byKey(const Key('loginBtn')));
await tester.pumpAndSettle(const Duration(seconds: 3));
expect(find.byType(HomeScreen), findsOneWidget);
});
testWidgets('invalid credentials surface an error', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email')), 'wrong@x.com');
await tester.enterText(find.byKey(const Key('password')), 'wrong');
await tester.tap(find.byKey(const Key('loginBtn')));
await tester.pumpAndSettle(const Duration(seconds: 3));
expect(find.text('Invalid credentials'), findsOneWidget);
});
});
}
# Run on the default device
flutter test integration_test
# Run on a specific device or simulator
flutter test integration_test --device-id=<id>
What integration tests are good for
| Use integration tests for | Use widget tests for |
|---|---|
| Login / signup / checkout / onboarding | Form validation rules |
| Flow spanning 3+ screens | Single screen behavior |
| Tests that need real platform APIs (camera permission UI, deep links) | Anything with fake API responses |
| Pre-release smoke suite | Per-feature regression coverage |
| CI on an emulator matrix | Every push (fast) |
Strategy — keep integration tests few and reliable
| Practice | Why |
|---|---|
Use Keys on widgets your tests find | find.byKey(...) survives copy / layout changes |
| Stub network at the boundary | Real network = flaky tests; use mockito / dio_adapter |
Hard-fail on slow operations (pumpAndSettle(timeout)) | Better than tests hanging forever |
| Run them on a real device in CI (Firebase Test Lab, BrowserStack) | Emulator-only misses platform-specific bugs |
| Tag them by criticality | Run smoke suite per PR, full suite nightly |
| Reset app state between tests (clear shared prefs, log out) | Bleed between tests is the #1 flake source |
Common mistakes to avoid
❌ Integration tests against the real production API
Flaky, slow, sometimes destructive. Stub at the network boundary.
❌ Treating integration tests like unit tests (cover every branch)
They're 100-1000× slower. Cover the critical paths; let unit tests
cover the edge cases.
❌ Using find.text() / find.byType() everywhere
Brittle to UI changes. Prefer Key-based finders for stable identity.
❌ pumpAndSettle() with no timeout
If something keeps animating (a periodic stream, infinite progress),
the test hangs forever. Always pass a Duration.
❌ Sharing state between integration tests
Each test should start from a clean slate — log out, clear storage,
reset providers.
❌ Running integration tests only locally
You need CI signal. Set up Firebase Test Lab / BrowserStack / GitHub Actions
on real devices.
Interview follow-ups
-
pumpAndSettlevspump— when do you use which?pump()advances by one frame (or a duration you pass).pumpAndSettle()keeps pumping until no frame is scheduled — useful after navigation or implicit animations. Risk: if something animates forever,pumpAndSettlehangs. UsepumpAndSettle(timeout: ...)to cap it. -
How do you mock HTTP in integration tests without changing app code? Two main options: (1) inject a fake
ApiClientvia DI /ProviderScope.overridesat app startup, or (2) useMockClient(package:http/testing.dart) ordio_test's adapter to intercept at the HTTP layer. Don't hit the real network. -
Where do widget tests stop and integration tests start? Widget tests render a piece of your app with mock dependencies and assert on UI behavior — they don't need a real device. Integration tests start your whole app via
app.main(), run on a real device/emulator, and test flows the user actually performs. The boundary is "full app on device." -
How do you run integration tests on CI across iOS and Android? GitHub Actions / GitLab CI for the Dart parts. For the actual device-runs, Firebase Test Lab is the standard option for cloud devices (Android matrix natively; iOS via the new Apple devices), or BrowserStack App Live. Both accept
flutter drive/flutter test integration_testoutput and run against real hardware.
How helpful was this content?
Please sign in to rate this article.