Integration Tests

Medium PriorityAsked in ~55% of senior interviews

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
 /────────────────\
LayerLives inRuns onSpeedCatches
Unittest/Dart VMFastLogic bugs
Widgettest/Dart + flutter_test bindingFastUI behavior, widget composition
Integrationintegration_test/Real device / emulator / webSlowEnd-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 forUse widget tests for
Login / signup / checkout / onboardingForm validation rules
Flow spanning 3+ screensSingle screen behavior
Tests that need real platform APIs (camera permission UI, deep links)Anything with fake API responses
Pre-release smoke suitePer-feature regression coverage
CI on an emulator matrixEvery push (fast)

Strategy — keep integration tests few and reliable

PracticeWhy
Use Keys on widgets your tests findfind.byKey(...) survives copy / layout changes
Stub network at the boundaryReal 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 criticalityRun 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

  1. pumpAndSettle vs pump — 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, pumpAndSettle hangs. Use pumpAndSettle(timeout: ...) to cap it.

  2. How do you mock HTTP in integration tests without changing app code? Two main options: (1) inject a fake ApiClient via DI / ProviderScope.overrides at app startup, or (2) use MockClient (package:http/testing.dart) or dio_test's adapter to intercept at the HTTP layer. Don't hit the real network.

  3. 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."

  4. 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_test output and run against real hardware.


How helpful was this content?

Please sign in to rate this article.