Dart Tutorial

Flutter Lesson 68 of 83 4 min read

Integration Testing in Flutter

Test complete user journeys in Flutter with the integration_test package: run the real app on a device, tap through screens and verify.

On this page

Unit and widget tests check pieces in isolation. An integration test runs the whole app on a real device or emulator, taps through it like a user, and checks that everything works together: navigation, plugins, storage and state.

They are slow, so write them only for the journeys that must never break: sign in, add to cart and check out, create and save a record.

Setup #

Add the package from the Flutter SDK:

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

Create a folder named integration_test at the project root, next to lib and test.

A first test #

// integration_test/app_test.dart
import 'package:flutter/material.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();

  testWidgets('a user can add a note and see it in the list', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    // Starts empty.
    expect(find.text('No notes yet'), findsOneWidget);

    // Open the editor.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pumpAndSettle();

    // Type and save.
    await tester.enterText(find.byType(TextField), 'Buy milk');
    await tester.tap(find.text('Save'));
    await tester.pumpAndSettle();

    // Back on the list, with the new note.
    expect(find.text('Buy milk'), findsOneWidget);
    expect(find.text('No notes yet'), findsNothing);
  });
}

The API is the same WidgetTester you used for widget tests. The differences are the first line, which connects the test to a real device, and that you start the real app with app.main().

Running it #

flutter test integration_test                       # all tests, on the connected device
flutter test integration_test/app_test.dart         # one file
flutter test integration_test -d emulator-5554      # a specific device

You will see the app open and operate itself.

A longer journey #

testWidgets('sign in, add to cart, check out', (tester) async {
  app.main();
  await tester.pumpAndSettle();

  // Sign in
  await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
  await tester.enterText(find.byKey(const Key('password')), 'password123');
  await tester.tap(find.widgetWithText(FilledButton, 'Sign in'));
  await tester.pumpAndSettle();
  expect(find.text('Products'), findsOneWidget);

  // Scroll to a product and add it
  await tester.scrollUntilVisible(find.text('Trail backpack'), 300);
  await tester.tap(find.text('Trail backpack'));
  await tester.pumpAndSettle();
  await tester.tap(find.text('Add to cart'));
  await tester.pumpAndSettle();

  // Check out
  await tester.tap(find.byTooltip('Cart'));
  await tester.pumpAndSettle();
  expect(find.text('Trail backpack'), findsOneWidget);
  await tester.tap(find.text('Check out'));
  await tester.pumpAndSettle();

  expect(find.text('Order placed'), findsOneWidget);
});

Making tests reliable #

Integration tests have a reputation for being flaky. Most of that comes from four causes.

1. A real backend. A slow or changed server fails the test for reasons unrelated to your code. Run against a test server with known data, or start the app with fake repositories:

// lib/main.dart
void main() => runApp(MyApp(api: RealApi()));

// integration_test/app_test.dart
await tester.pumpWidget(MyApp(api: FakeApi()));

2. State left over from the last run. Clear saved data at the start of each test, or use a fresh account.

3. Waiting for the wrong thing. pumpAndSettle waits until animations stop. It does not wait for a network response on its own if nothing is animating, and it times out if something animates for ever. To wait for a specific widget:

Future<void> waitFor(WidgetTester tester, Finder finder,
    {Duration timeout = const Duration(seconds: 10)}) async {
  final end = DateTime.now().add(timeout);
  while (DateTime.now().isBefore(end)) {
    await tester.pump(const Duration(milliseconds: 100));
    if (finder.evaluate().isNotEmpty) return;
  }
  throw TestFailure('Timed out waiting for $finder');
}

4. Fragile finders. Text changes with wording and language. Give important widgets a Key and find by that.

System dialogs #

Permission prompts, the photo picker and notifications are drawn by the operating system, outside Flutter, so tester cannot tap them. Options:

  • Grant permissions before the test, with adb shell pm grant on Android.
  • Use the patrol package, which extends integration tests with the ability to interact with native dialogs.

Measuring performance #

An integration test can record frame timings while it scrolls, to catch performance regressions.

final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

await binding.traceAction(() async {
  await tester.fling(find.byType(ListView), const Offset(0, -3000), 5000);
  await tester.pumpAndSettle();
}, reportKey: 'scrolling');

Run this in profile mode on a real device.

Running on many devices #

Emulators on your machine cover a few configurations. Firebase Test Lab and similar services run your integration tests on dozens of real devices and return videos and logs. They are usually part of a CI pipeline.

How many? #

A sensible split for most apps:

KindShareCovers
UnitMostLogic, models, state holders
WidgetManyScreens in each of their states
IntegrationA handfulThe three to five journeys the business depends on

Try it yourself #

Write one integration test for an app you have built in this course. It should start the app, perform the main task from beginning to end across at least two screens, and check the final result. Run it on an emulator and watch it work.

Practise in the playground Updated by Santosh Adhikari