Contents
- Project Setup
- Test Authoring
- Execution Targets
- Performance Profiling
- CI/CD Integration
- Common Pitfalls
- Workflow: Adding an Integration Test
- Examples
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)
# Terminal 1: Start ChromeDriver
chromedriver --port=4444
# Terminal 2: Run tests
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)
# 1. Build debug APK
flutter build apk --debug
# 2. Build instrumentation test APK
pushd android && ./gradlew app:assembleAndroidTest && popd
# 3. Upload both APKs to Firebase Test Lab via console or gcloud:
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-runnerfor Android emulator. - Collect test result artifacts with
actions/upload-artifact. - For web, run
chromedriveras 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
- [ ] Step 1: Add
integration_testandflutter_testtodev_dependencies. - [ ] Step 2: Assign
ValueKeys to target widgets in production code. - [ ] Step 3: Create
integration_test/app_test.dartwith binding initialization. - [ ] Step 4: Create
test_driver/integration_test.dartwithintegrationDriver(). - [ ] Step 5: Write test cases — load app, interact, assert.
- [ ] Step 6: Choose execution target:
- Local device:
flutter test integration_test/ - Chrome:
flutter drive ... -d chrome - Firebase Test Lab: build + upload APKs
- Local device:
- [ ] Step 7: Feedback Loop:
- If
PumpAndSettleTimedOutException→ check for infinite animations. - If widget not found → add
scrollUntilVisible. - Re-run until all tests pass.
- If
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);
});