In Flutter, how does an integration_test test differ from a widget test, and how do you write and run one on an Android emulator?
answer
- same testWidgets API, real app
- IntegrationTestWidgetsFlutterBinding.ensureInitialized first
- integration_test/ folder, sdk dev dependency
- flutter test integration_test -d
- real time, real plugins, slower
basics
~20 sAn integration_test test uses the same testWidgets and finder API, but runs the real app on a device or emulator in real time with real plugins. Call IntegrationTestWidgetsFlutterBinding.ensureInitialized() first and run flutter test integration_test with -d.
solid answer
~50 sThe code looks like a widget test - `testWidgets`, `WidgetTester`, `find`, `expect` - but it runs inside the built app on a real device or emulator. I add `integration_test` and `flutter_test` as `sdk: flutter` dev dependencies, put files like `integration_test/checkout_test.dart` in that folder, and start `main` with `IntegrationTestWidgetsFlutterBinding.ensureInitialized()`, the binding that reports results back to the runner. The test starts the real app (`app.main()` or `pumpWidget(const ShopApp())`), taps through the checkout and asserts the confirmation. It runs with `flutter test integration_test/checkout_test.dart -d emulator-5554`, which builds the app, installs it and fails with 'No devices are connected' when there is none. Differences from a widget test: time is real, so `pump(duration)` actually waits; platform channels, plugins, rendering and the network are real; and each run costs a build and an install, so these tests are few.
code
dart · 24 linesimport 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:shop/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('guest checkout places an order', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('product-0-add')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('cart-checkout')));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('checkout-email')), '[email protected]');
await tester.tap(find.byKey(const Key('checkout-place-order')));
await tester.pumpAndSettle();
expect(find.text('Order confirmed'), findsOneWidget);
}, timeout: const Timeout(Duration(minutes: 3)));
}go deeper
Recall the setup: integration_test and flutter_test as SDK dev dependencies, files in integration_test/, the ensureInitialized call, and flutter test integration_test with -d.
Explain what changes on a device: a live binding with real time, real plugins and fonts, no default timeout, and results reported back to the runner by the integration binding.
Make the checkout journey deterministic with seeded data and bounded waits, give every test a timeout, and keep integration tests to the few journeys that justify their cost.
Decide which user journeys earn on-device coverage and what the team pays per run in build, install and real-time execution.
## Same API, different world The **`integration_test`** package, shipped with the Flutter SDK, lets you write tests with the familiar **`flutter_test`** API - `testWidgets`, `WidgetTester`, finders, `expect` - and run them **inside the real app on a real device, emulator, simulator or desktop**. A widget test, by contrast, runs a slice of UI in the host's `flutter_tester` process with fake time, test fonts and no real plugins. | Aspect | Widget test (`test/`) | Integration test (`integration_test/`) | |---|---|---| | Where it runs | host `flutter_tester` process | the built app on a device or emulator | | Binding | `AutomatedTestWidgetsFlutterBinding` | `IntegrationTestWidgetsFlutterBinding` (a `LiveTestWidgetsFlutterBinding`) | | Time | fake; `pump(duration)` is instant | real; `pump(duration)` waits | | Plugins and channels | absent unless mocked | real native implementations | | Fonts and rendering | test font, software rendering | the app's fonts and the device's renderer | | Default per-test timeout | 10 minutes | none | | Cost per run | milliseconds per test | a build, an install, then real-time execution | ## Setting it up 1. Add the dev dependencies: - `integration_test: sdk: flutter` - `flutter_test: sdk: flutter` 2. Create an `integration_test/` directory at the package root, next to `test/`. 3. In each file, call **`IntegrationTestWidgetsFlutterBinding.ensureInitialized()`** at the top of `main`, before any `testWidgets`. This binding extends the live binding and **adapts `flutter_test` results for the runner** - the `flutter` tool, a `flutter drive` driver script, or native Android instrumentation - and provides extras such as `takeScreenshot` and `traceAction`. 4. Start the app under test: call the app's `main()` (with any test-specific configuration) or `pumpWidget` its root widget. ## Running it - `flutter test integration_test` runs every test file in the folder on the selected device; pass one file to run just that. - Choose the target with `-d`, for example `-d emulator-5554` for a running Android emulator. With no device the tool exits with "No devices are connected". - `--flavor` is supported for Android, Linux, macOS and iOS devices. - The tool reports results itself (it sets `INTEGRATION_TEST_SHOULD_REPORT_RESULTS_TO_NATIVE=false`), so output looks like a normal `flutter test` run. - Web devices are **not** supported by `flutter test` for integration tests; the web path is `flutter drive` with a WebDriver server. ## Writing the checkout journey - Seed deterministic data: a test backend, a fixed catalogue, a known account. Real network means real flakiness. - Wait for real work with pumps rather than sleeps: a short loop of `pump(const Duration(milliseconds: 100))` until a finder matches, with an upper bound, is clearer than a fixed delay. - `pumpAndSettle()` works too, but its steps now take real time, and it still never settles while an endless animation (a loading spinner) runs. - Give each test its own timeout (`testWidgets(..., timeout: const Timeout(Duration(minutes: 3)))`), because the integration binding sets **no default timeout** and a stuck test otherwise holds the CI job until the job itself times out. ## Where it stops A finder can only see **Flutter's widget tree**. Native permission dialogs, the system share sheet, notifications and other apps are outside it, so the test cannot tap them with `flutter_test` APIs. That boundary - and how to work around it - is the most common follow-up interviewers ask. ## Cost and placement Each run builds the app for the device, installs it and executes in real time, so an integration test costs seconds to minutes where a widget test costs milliseconds. Teams keep a few journeys - sign-in, checkout, onboarding - and leave detailed UI behaviour to widget tests.
- Why does pump(const Duration(seconds: 2)) behave differently here than in a widget test?The integration binding is a live binding: `pump(duration)` really waits that long before the frame, because time is not simulated. In a widget test the same call advances a fake clock instantly. Real waiting makes integration tests slower and sensitive to device speed, so prefer waiting for a finder with a bounded loop.
- Can you run these integration tests in Chrome with flutter test?No. `flutter test` exits with 'Web devices are not supported for integration tests yet.' For the web you run a WebDriver server such as chromedriver and use `flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d web-server` (or `-d chrome`).
- What happens if a checkout step hangs, for example a request that never returns?The integration binding's default test timeout is none, so the test waits until something external - the CI job limit - kills it, often without a useful message. Set a `timeout` on `testWidgets` (or `--timeout` on `flutter drive`) so the failure is reported by the test with its own output.
saying these in an interview costs you the question
- Integration tests use fake time just like widget tests
- flutter test integration_test runs without any connected device
- The IntegrationTestWidgetsFlutterBinding call is optional boilerplate
- Platform plugins are mocked automatically in integration tests
- Every widget-level behaviour should be covered by an integration test