Files
Musify/musify_flutter/.agents/skills/flutter-testing/references/integration-testing.md
T

563 lines
14 KiB
Markdown

# 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`:
```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:
```dart
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
```
## Test Structure
### Basic Integration 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';
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
```bash
# 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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
1. **Test critical user paths** - Focus on important flows
2. **Keep tests independent** - Each test should be standalone
3. **Use descriptive names** - Explain what the test does
4. **Wait for async operations** - Use `pumpAndSettle()`
5. **Test real behavior** - Verify actual device behavior, not mocks
6. **Measure performance** - Track frame times, memory, and build counts
7. **Test edge cases** - Offline, errors, empty states
8. **Keep tests fast** - Avoid unnecessary delays
## Debugging Integration Tests
### Take Screenshots
```dart
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
```dart
testWidgets('print widget tree for debugging', (tester) async {
await tester.pumpWidget(const MyApp());
// Print widget tree
debugDumpApp();
});
```
## CI/CD Integration
### GitHub Actions
```yaml
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
```bash
# Generate test report
flutter test integration_test/ --reporter expanded
# Generate JSON report
flutter test integration_test/ --reporter json > test-results.json
```