1. Setting Up integration_test
flutter pub add --dev integration_test --sdk=flutter
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:myapp/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('full add-to-cart flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// the rest of the flow goes here
});
}
flutter test integration_test/app_test.dart
Unlike Week 16's widget tests, this runs the real main() entry point on
a real device or emulator — genuinely exercising platform channels, real animations,
and real navigation, not a simulated test-only environment.
2. A Multi-Screen Flow
testWidgets('browse, add to cart, and see the updated badge', (tester) async {
app.main();
await tester.pumpAndSettle();
// tap the first product
await tester.tap(find.byKey(const Key('product-0')));
await tester.pumpAndSettle();
// add it to the cart from the detail screen
await tester.tap(find.text('Add to Cart'));
await tester.pumpAndSettle();
// navigate back
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
// confirm the badge updated
expect(find.text('1'), findsOneWidget);
});
find.byKey is worth using deliberately for anything an integration test
needs to target reliably — a stable Key('product-0') survives copy
changes and localization the way matching by visible text does not, the same
reasoning behind testID in this site's React Native course.
3. pumpAndSettle for Async & Animated UI
pumpAndSettle() repeatedly pumps frames until no more are scheduled —
the right tool once an animation, a network call, or any delayed state change is
involved, which is most of a real app.
await tester.tap(find.text('Load Products'));
await tester.pumpAndSettle(const Duration(seconds: 5)); // a genuine timeout ceiling
expect(find.byType(ListTile), findsWidgets);
A plain pump() only advances one frame — insufficient for anything
still animating or awaiting a future. But pumpAndSettle itself times
out and throws if something never stops scheduling frames (a repeating animation,
like Week 14's pulsing icon, that legitimately never "settles") — worth knowing
before spending time debugging what looks like a hang.
4. Running Integration Tests in CI
jobs:
integration-test:
runs-on: macos-latest # needed for hardware-accelerated emulation
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
script: flutter test integration_test/app_test.dart
Like Week 16's widget tests, an integration test needs a genuinely running device or emulator to execute against — meaningfully more CI setup and time than a unit or widget test suite, which is why most teams run the fast suite (Weeks 16's unit/widget tests) on every push and reserve integration tests for merges into a main branch or a nightly run, the same tradeoff this site's React Native course makes with Detox.
5. Hands-on Exercise
Write an end-to-end integration test
Drive the real app through a multi-screen flow, end to end.
Requirements:
- Set up
integration_testand confirm a minimal test (just launching the app and asserting one widget is present) runs successfully on a real device or emulator. - Add stable
Keys to at least 3 elements the test needs to target reliably. - A full flow test: browse to a product, add it to the cart, navigate back, and assert the cart badge updated correctly.
- Confirm the test correctly uses
pumpAndSettleafter every navigation and async step, with no flaky failures across at least 3 consecutive runs. - Document in a short comment what CI setup (a macOS runner for iOS, an Android emulator action for Android) would be needed to run this in CI, even if you don't wire up the actual CI job.
If pumpAndSettle() throws claiming it timed out, check whether anything on screen is a genuinely infinite/repeating animation (like a looping spinner or Week 14's pulsing icon) — pumpAndSettle waits for frames to stop being scheduled entirely, which a truly indefinite animation never does; in that case, use a bounded plain pump(duration) instead of pumpAndSettle around that specific screen.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What does an integration test exercise that a widget test (Week 16) does not?
What does an integration test exercise that a widget test (Week 16) does not?
The real, compiled app running on an actual device or emulator — real platform channels, real native rendering, real navigation transitions, and the actual main() entry point — rather than a simulated in-memory test environment that stubs out platform-level behavior. It's a genuinely different, higher-fidelity kind of test, at the cost of being much slower to run.
Q2
Why is targeting elements with find.byKey often preferred over find.text in an integration test meant to be stable over time?
Why is targeting elements with find.byKey often preferred over find.text in an integration test meant to be stable over time?
A Key is a deliberate, stable identifier that doesn't change with copy edits, localization, or minor UI rewording — matching by visible text ties the test to exact wording, which breaks the test on changes that have nothing to do with whether the actual flow still works correctly.
Q3
Why can pumpAndSettle() throw a timeout error on a screen with a legitimately infinite animation, and what should you do about it?
Why can pumpAndSettle() throw a timeout error on a screen with a legitimately infinite animation, and what should you do about it?
pumpAndSettle keeps pumping frames until none are scheduled — an indefinitely looping animation (like Week 14's pulsing icon) never reaches that state on its own, so pumpAndSettle eventually gives up and throws. The fix is to use a bounded plain pump(duration) for a fixed wait around that specific screen instead of expecting the animation to ever "settle."
Q4
Why do most teams run integration tests less frequently (e.g. only on merge to main) than the unit/widget test suite?
Why do most teams run integration tests less frequently (e.g. only on merge to main) than the unit/widget test suite?
An integration test needs an actual device or emulator running, which takes meaningfully longer to set up and execute than an in-memory widget test — running the full integration suite on every single push would slow down every contributor's feedback loop for a cost that isn't worth paying that often. The fast unit/widget suite catches most regressions cheaply on every push; integration tests are reserved for checkpoints where that higher-fidelity, real-device check is worth the wait.