chore: bring flutter client, docs, deploy and tools into monorepo
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user