14 KiB
14 KiB
Integration Testing Guide
Overview
Integration tests test complete apps or large parts of apps on real devices or emulators. They verify that all widgets and services work together as expected and can measure performance.
When to Write Integration Tests
- Testing complete user flows across multiple screens
- Verifying navigation between pages
- Testing state persistence
- Validating real device behavior (sensors, camera, etc.)
- Performance profiling and benchmarking
- Testing platform-specific features
Setup
Add Dependency
Add to pubspec.yaml:
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
Create Test Driver
Create test_driver/integration_test.dart when using flutter drive, browser
integration tests, screenshots, or response data from performance traces:
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
Test Structure
Basic 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();
testWidgets('app launches and displays home', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('Home Screen'), findsOneWidget);
});
}
Running Integration Tests
# Run on all devices
flutter test integration_test/
# Run on specific device
flutter test -d <device-id> integration_test/my_test.dart
# Run specific test file
flutter test integration_test/my_test.dart
# Run a browser/driver integration test
flutter drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/my_test.dart \
-d chrome
Testing User Flows
Multi-Screen Navigation
testWidgets('complete user flow from login to dashboard', (tester) async {
await tester.pumpWidget(const MyApp());
// Login screen
expect(find.text('Login'), findsOneWidget);
// Enter credentials
await tester.enterText(find.byKey(const Key('username')), 'user@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password123');
// Tap login
await tester.tap(find.byKey(const Key('login-button')));
await tester.pumpAndSettle();
// Dashboard screen
expect(find.text('Dashboard'), findsOneWidget);
expect(find.text('Welcome, user@example.com'), findsOneWidget);
});
Tab Navigation
testWidgets('navigate between tabs', (tester) async {
await tester.pumpWidget(const MyApp());
// Initial tab
expect(find.text('Home'), findsOneWidget);
// Tap second tab
await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsNothing);
expect(find.text('Profile'), findsOneWidget);
// Navigate back
await tester.tap(find.text('Home'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
});
Testing Forms
Complete Form Submission
testWidgets('submit registration form', (tester) async {
await tester.pumpWidget(const MyApp());
// Navigate to registration
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
// Fill form
await tester.enterText(find.byKey(const Key('name')), 'John Doe');
await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password123');
await tester.enterText(find.byKey(const Key('confirm-password')), 'password123');
// Submit
await tester.tap(find.byKey(const Key('submit')));
await tester.pumpAndSettle();
// Verify success
expect(find.text('Registration Successful'), findsOneWidget);
});
Form Validation
testWidgets('form shows validation errors', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
// Submit empty form
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
// Verify errors
expect(find.text('Name is required'), findsOneWidget);
expect(find.text('Email is required'), findsOneWidget);
expect(find.text('Password is required'), findsOneWidget);
// Fill with invalid email
await tester.enterText(find.byKey(const Key('email')), 'invalid-email');
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
expect(find.text('Invalid email format'), findsOneWidget);
});
Testing Lists and Data
Loading and Displaying Data
testWidgets('load and display items from API', (tester) async {
await tester.pumpWidget(const MyApp());
// Wait for data load
await tester.pumpAndSettle();
// Verify items loaded
expect(find.byType(ListTile), findsWidgets);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
});
Pull-to-Refresh
testWidgets('pull to refresh updates list', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
// Pull down
await tester.drag(
find.byType(RefreshIndicator),
const Offset(0, 300),
);
await tester.pumpAndSettle();
// Verify refreshed data
expect(find.text('Updated Item 1'), findsOneWidget);
});
Infinite Scrolling
testWidgets('load more items on scroll', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
// Initial items
expect(find.text('Item 20'), findsNothing);
// Scroll to bottom to trigger load more
await tester.fling(
find.byType(ListView),
const Offset(0, -2000),
10000,
);
await tester.pumpAndSettle();
// New items loaded
expect(find.text('Item 20'), findsOneWidget);
});
Testing State Persistence
Persisted Settings
testWidgets('user preference persists across app rebuild', (tester) async {
final preferences = FakePreferencesStore();
await tester.pumpWidget(MyApp(preferences: preferences));
// Change setting
await tester.tap(find.text('Dark Mode'));
await tester.pumpAndSettle();
// Rebuild the app with the same injected storage.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpWidget(MyApp(preferences: preferences));
await tester.pumpAndSettle();
// Verify setting persisted
expect(find.text('Light Mode'), findsNothing);
expect(find.text('Dark Mode'), findsOneWidget);
});
Authentication State
testWidgets('user stays logged in', (tester) async {
final authStore = FakeAuthStore();
await tester.pumpWidget(MyApp(authStore: authStore));
// Login
await tester.enterText(find.byKey(const Key('email')), 'user@example.com');
await tester.enterText(find.byKey(const Key('password')), 'password');
await tester.tap(find.byKey(const Key('login')));
await tester.pumpAndSettle();
expect(find.text('Dashboard'), findsOneWidget);
// Rebuild the app with the same injected auth store.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpWidget(MyApp(authStore: authStore));
await tester.pumpAndSettle();
// Still logged in
expect(find.text('Dashboard'), findsOneWidget);
expect(find.text('Login'), findsNothing);
});
Performance Testing
Measuring Frame Time
testWidgets('measure scrolling performance', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
await binding.watchPerformance(() async {
await tester.fling(
find.byType(ListView),
const Offset(0, -1000),
5000,
);
await tester.pumpAndSettle();
}, reportKey: 'scrolling_performance');
});
Tracking Build Count
testWidgets('widget doesn't rebuild unnecessarily', (tester) async {
int buildCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
buildCount++;
return const MyWidget();
},
),
),
);
expect(buildCount, 1);
// Perform unrelated action
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// Should not rebuild
expect(buildCount, 1);
});
Memory Tracking on Native Targets
import 'dart:io' show ProcessInfo;
testWidgets('monitor memory usage during scrolling', (tester) async {
await tester.pumpWidget(const MyApp());
final initialMemory = ProcessInfo.currentRss;
await tester.fling(
find.byType(ListView),
const Offset(0, -5000),
10000,
);
await tester.pumpAndSettle();
final finalMemory = ProcessInfo.currentRss;
final memoryIncrease = finalMemory - initialMemory;
// Memory increase should be reasonable (< 10MB)
expect(memoryIncrease, lessThan(10 * 1024 * 1024));
});
Testing Platform-Specific Features
Permissions
testWidgets('request and handle camera permission', (tester) async {
final permissions = FakePermissionGateway(
camera: PermissionStatus.granted,
);
await tester.pumpWidget(MyApp(permissions: permissions));
// Request camera access
await tester.tap(find.text('Take Photo'));
await tester.pumpAndSettle();
// Verify camera screen appears
expect(find.byType(CameraPreview), findsOneWidget);
});
Deep Links
testWidgets('handle deep link to product page', (tester) async {
await tester.pumpWidget(const MyApp(initialRoute: '/product/123'));
await tester.pumpAndSettle();
expect(find.text('Product #123'), findsOneWidget);
});
In-App Purchases
testWidgets('complete purchase flow', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.text('Premium'));
await tester.pumpAndSettle();
await tester.tap(find.text('Subscribe'));
await tester.pumpAndSettle();
// Verify purchase completed
expect(find.text('Premium Active'), findsOneWidget);
});
Testing Network
Offline Behavior
testWidgets('handle offline state gracefully', (tester) async {
final connectivity = FakeConnectivity(isOnline: false);
final api = FakeApiClient(networkError: const SocketException('offline'));
await tester.pumpWidget(MyApp(connectivity: connectivity, apiClient: api));
await tester.pumpAndSettle();
// Verify offline message
expect(find.text('No internet connection'), findsOneWidget);
// Simulate back online
connectivity.isOnline = true;
api.networkError = null;
await tester.tap(find.byKey(const Key('retry-button')));
await tester.pumpAndSettle();
// Reload data
expect(find.text('Data loaded'), findsOneWidget);
});
Error Handling
testWidgets('handle network errors', (tester) async {
await tester.pumpWidget(const MyApp());
// Trigger API call
await tester.tap(find.text('Load Data'));
await tester.pumpAndSettle();
// Verify error message displayed
expect(find.text('Failed to load data'), findsOneWidget);
expect(find.byKey(const Key('retry-button')), findsOneWidget);
});
Testing Animations
Smooth Animations
testWidgets('animation runs smoothly', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(const MyApp());
await binding.traceAction(() async {
await tester.tap(find.byKey(const Key('animate')));
await tester.pumpAndSettle();
}, reportKey: 'animation_timeline');
});
Best Practices
- Test critical user paths - Focus on important flows
- Keep tests independent - Each test should be standalone
- Use descriptive names - Explain what the test does
- Wait for async operations - Use
pumpAndSettle() - Test real behavior - Verify actual device behavior, not mocks
- Measure performance - Track frame times, memory, and build counts
- Test edge cases - Offline, errors, empty states
- Keep tests fast - Avoid unnecessary delays
Debugging Integration Tests
Take Screenshots
testWidgets('debug test with screenshot', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(const MyApp());
// On Android, call convertFlutterSurfaceToImage and pump before screenshots.
await binding.convertFlutterSurfaceToImage();
await tester.pump();
await binding.takeScreenshot('test-screenshot');
// Perform actions
await tester.tap(find.text('Button'));
await tester.pumpAndSettle();
// Take another screenshot
await binding.takeScreenshot('after-tap');
});
Print Widget Tree
testWidgets('print widget tree for debugging', (tester) async {
await tester.pumpWidget(const MyApp());
// Print widget tree
debugDumpApp();
});
CI/CD Integration
GitHub Actions
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0'
- name: Get dependencies
run: flutter pub get
- name: Run integration tests
run: flutter test integration_test/
Test Reports
# Generate test report
flutter test integration_test/ --reporter expanded
# Generate JSON report
flutter test integration_test/ --reporter json > test-results.json