skip to content

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?

level: juniorimportance: should knowfreq 48%

answer

  1. same testWidgets API, real app
  2. IntegrationTestWidgetsFlutterBinding.ensureInitialized first
  3. integration_test/ folder, sdk dev dependency
  4. flutter test integration_test -d
  5. real time, real plugins, slower

basics

~20 s

An 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 s

The 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 lines
dart
import '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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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