| name | flutter-add-integration-test |
| description | Configure and run integration tests using the integration_test package with Flutter Driver. Use when testing complete user flows, verifying navigation, or running end-to-end tests on devices or CI. |
| metadata | {"platforms":"flutter","languages":"dart","category":"testing"} |
Contents
Project Setup
-
Add required development dependencies to pubspec.yaml:
flutter pub add 'dev:integration_test:{"sdk":"flutter"}'
flutter pub add 'dev:flutter_test:{"sdk":"flutter"}'
-
Create directory structure:
project_root/
├── integration_test/
│ └── app_test.dart # Test cases
└── test_driver/
└── integration_test.dart # Host driver script
-
Create the host driver script at test_driver/integration_test.dart:
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
-
Add ValueKeys to critical widgets in production code for reliable targeting:
FloatingActionButton(
key: const ValueKey('increment_fab'),
onPressed: _increment,
child: const Icon(Icons.add),
)
Test Authoring
- Initialize the binding at the top of
main() — this replaces the default test binding.
- Load the full application with
tester.pumpWidget(const MyApp()).
- Use
tester.pumpAndSettle() after every interaction to wait for animations and async operations.
- Assert widget visibility using
expect(find.byKey(ValueKey('foo')), findsOneWidget).
- Scroll to off-screen widgets using
tester.scrollUntilVisible(finder, 500.0).
Test File Structure
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';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('End-to-end test', () {
testWidgets('complete user flow', (tester) async {
// Load full app
await tester.pumpWidget(const MyApp());
// Interact with widgets
await tester.tap(find.byKey(const ValueKey('login_button')));
await tester.pumpAndSettle();
// Assert navigation happened
expect(find.byType(HomePage), findsOneWidget);
});
});
}
Execution Targets
Choose the execution method based on target platform:
Local Device (Android/iOS)
flutter test integration_test/
For iOS, when running under the default Swift Package Manager (SPM) integration (Flutter 3.44+), ensure you run package resolution first:
xcodebuild -resolvePackageDependencies -workspace ios/Runner.xcworkspace -scheme Runner
Chrome (Web)
chromedriver --port=4444
flutter drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/app_test.dart \
-d chrome
Headless Web
flutter drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/app_test.dart \
-d web-server
Firebase Test Lab (Android)
flutter build apk --debug
pushd android && ./gradlew app:assembleAndroidTest && popd
gcloud firebase test android run \
--type instrumentation \
--app build/app/outputs/flutter-apk/app-debug.apk \
--test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk
Performance Profiling
Wrap test actions in binding.traceAction() to capture performance timelines:
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('scrolling performance', (tester) async {
await tester.pumpWidget(const MyApp());
await binding.traceAction(() async {
final listFinder = find.byType(Scrollable);
await tester.fling(listFinder, const Offset(0, -500), 10000);
await tester.pumpAndSettle();
}, reportKey: 'scrolling_timeline');
});
}
Performance Profiling Driver
Use this driver to capture and write timeline data:
import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() {
return integrationDriver(
responseDataCallback: (data) async {
if (data != null) {
final timeline = driver.Timeline.fromJson(
data['scrolling_timeline'] as Map<String, dynamic>,
);
final summary = driver.TimelineSummary.summarize(timeline);
await summary.writeTimelineToFile(
'scrolling_timeline',
pretty: true,
includeSummary: true,
);
}
},
);
}
CI/CD Integration
GitHub Actions Workflow
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
script: flutter test integration_test/ --flavor dev
- Use
reactivecircus/android-emulator-runner for Android emulator.
- Collect test result artifacts with
actions/upload-artifact.
- For web, run
chromedriver as a service and test with -d chrome.
Common Pitfalls
| Error | Cause | Fix |
|---|
PumpAndSettleTimedOutException | Infinite animation (e.g., CircularProgressIndicator) | Use pump() instead, or dismiss the loading state |
| Widget not found | Lazy-loaded in SliverList or ListView | Call scrollUntilVisible() before interacting |
| Test hangs | Network call in production code | Mock HTTP client or use --dart-define to bypass |
No host driver specified | Missing test_driver/integration_test.dart | Create the host driver file |
Workflow: Adding an Integration Test
Task Progress
Examples
Standard Integration Test
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';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Counter app', () {
testWidgets('tap FAB, verify counter increments', (tester) async {
await tester.pumpWidget(const MyApp());
// Verify initial state
expect(find.text('0'), findsOneWidget);
// Tap the increment button
final fab = find.byKey(const ValueKey('increment_fab'));
await tester.tap(fab);
await tester.pumpAndSettle();
// Verify counter incremented
expect(find.text('1'), findsOneWidget);
});
});
}
Multi-Screen Navigation Flow
testWidgets('login and navigate to home', (tester) async {
await tester.pumpWidget(const MyApp());
// Enter credentials
await tester.enterText(find.byKey(const ValueKey('email_field')), 'user@test.com');
await tester.enterText(find.byKey(const ValueKey('password_field')), 'password123');
// Submit login
await tester.tap(find.byKey(const ValueKey('login_button')));
await tester.pumpAndSettle();
// Verify navigation to home
expect(find.byType(HomePage), findsOneWidget);
expect(find.byType(LoginPage), findsNothing);
});