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:
2026-07-05 03:22:24 +02:00
co-authored by Claude Opus 4.8
parent 8c0519d125
commit 4b80c7dbb0
189 changed files with 19124 additions and 0 deletions
@@ -0,0 +1,507 @@
# Common Testing Errors
## Overview
This guide covers frequently encountered Flutter testing errors and their solutions.
## Layout Errors
### 'A RenderFlex overflowed...'
**Error Message:**
```
The following assertion was thrown during layout:
A RenderFlex overflowed by 1146 pixels on the right.
```
**Cause:** Yellow and black stripes indicate overflow when a child widget is too large for its parent (Row/Column).
**Solution:** Wrap the overflowing widget in `Expanded` or `Flexible`.
```dart
// Problem
Row(
children: [
Icon(Icons.message),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
Text('Very long text that overflows the available space...'),
],
),
],
)
// Solution
Row(
children: [
Icon(Icons.message),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
Text('Very long text that fits in the available space...'),
],
),
),
],
)
```
### 'Vertical viewport was given unbounded height'
**Error Message:**
```
Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container.
This situation typically happens when a scrollable widget is nested inside another scrollable widget.
```
**Cause:** ListView or other scrollable widget inside Column without height constraints.
**Solution:** Wrap in `Expanded` or use `shrinkWrap: true`.
```dart
// Problem
Column(
children: [
Text('Header'),
ListView(
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
)
// Solution 1: Expanded
Column(
children: [
Text('Header'),
Expanded(
child: ListView(
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
),
],
)
// Solution 2: shrinkWrap
Column(
children: [
Text('Header'),
ListView(
shrinkWrap: true,
children: [
ListTile(leading: Icon(Icons.map), title: Text('Map')),
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
],
),
],
)
```
### 'An InputDecorator...cannot have an unbounded width'
**Error Message:**
```
An InputDecorator, which is typically created by a TextField, cannot have an unbounded width.
This happens when the parent widget does not provide a finite width constraint.
```
**Cause:** TextField or TextFormField inside Row without width constraints.
**Solution:** Wrap in `Expanded` or `SizedBox`.
```dart
// Problem
Row(
children: [TextField()],
)
// Solution 1: Expanded
Row(
children: [Expanded(child: TextFormField())],
)
// Solution 2: SizedBox
Row(
children: [SizedBox(width: 200, child: TextFormField())],
)
```
## Widget Lifecycle Errors
### 'setState called during build'
**Error Message:**
```
setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework
is already in the process of building widgets.
```
**Cause:** Calling `setState` (or methods that call it like `showDialog`) directly in the `build` method.
**Solution:** Use `WidgetsBinding.instance.addPostFrameCallback` or Navigator API.
```dart
// Problem
Widget build(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text('Error!')),
);
return Center(child: Text('Show Dialog'));
}
// Solution 1: Post-frame callback
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) => AlertDialog(title: Text('Success!')),
);
});
return Center(child: Text('Show Dialog'));
}
// Solution 2: Navigator API (for initial navigation)
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => DialogScreen(),
),
);
},
child: Text('Show Dialog'),
);
}
```
## Parent Widget Errors
### 'Incorrect use of ParentData widget'
**Error Message:**
```
The following assertion was thrown while looking for parent data:
Incorrect use of ParentDataWidget.
Usually, this indicates that at least one of the offending ParentDataWidgets
listed above is not placed directly inside a compatible ancestor widget.
```
**Cause:** Using widgets that require specific parent widgets in wrong context.
**Common Solutions:**
| Widget | Expected Parent | Solution |
|---------------------|---------------------|-----------------------------------|
| `Flexible` | `Row`, `Column`, `Flex` | Ensure parent is Row/Column/Flex |
| `Expanded` | `Row`, `Column`, `Flex` | Ensure parent is Row/Column/Flex |
| `Positioned` | `Stack` | Wrap in Stack |
| `TableCell` | `Table` | Ensure parent is Table |
```dart
// Problem
Column(
children: [
Expanded(child: Text('Wrong!')), // Expanded needs Row/Column parent, not Column children
],
)
// Solution
Column(
children: [
Row(
children: [
Expanded(child: Text('Correct!')),
],
),
],
)
```
## Testing-Specific Errors
### 'WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp'
**Error Message:**
```
WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp.
```
**Cause:** Pumping widget without MaterialApp context.
**Solution:** Wrap widget in MaterialApp.
```dart
// Problem
testWidgets('test without MaterialApp', (tester) async {
await tester.pumpWidget(MyWidget());
});
// Solution
testWidgets('test with MaterialApp', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(),
),
);
});
```
### 'No Finder found'
**Error Message:**
```
No Finder found.
Test failed: No elements found matching the finder.
```
**Cause:** Widget not found in tree at time of test.
**Common Solutions:**
```dart
// 1. Wrong text/content
expect(find.text('Wrong Text'), findsNothing); // Error
// Solution: Use correct text
expect(find.text('Correct Text'), findsOneWidget);
// 2. Widget not built yet
testWidgets('widget not built', (tester) async {
await tester.pumpWidget(MyWidget());
expect(find.text('Async Text'), findsOneWidget); // Error
});
// Solution: Pump and settle for async operations
testWidgets('widget not built', (tester) async {
await tester.pumpWidget(MyWidget());
await tester.pumpAndSettle(); // Wait for async operations
expect(find.text('Async Text'), findsOneWidget);
});
// 3. Wrong finder type
testWidgets('wrong finder', (tester) async {
await tester.pumpWidget(TextButton(child: Text('Button')));
await tester.tap(find.byType(ElevatedButton)); // Error
});
// Solution: Use correct type or use Text finder
testWidgets('correct finder', (tester) async {
await tester.pumpWidget(TextButton(child: Text('Button')));
await tester.tap(find.byType(TextButton)); // Correct
});
```
## Plugin/Platform Errors
### 'MissingPluginException'
**Error Message:**
```
MissingPluginException(No implementation found for method MethodName on channel channel.name)
```
**Cause:** Plugin not registered or mock not set up in tests.
**Solution:** Mock the method channel in tests.
```dart
import 'package:flutter/services.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
(MethodCall methodCall) async {
if (methodCall.method == 'getPlatformVersion') {
return 'Android 12';
}
throw MissingPluginException();
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('your.plugin.channel'),
null,
);
});
}
```
## Async Errors
### 'TimeoutException'
**Error Message:**
```
TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.
```
**Cause:** Async operation not completing or `pumpAndSettle()` waiting indefinitely.
**Solution:** Set timeout or fix infinite loops.
```dart
// Solution 1: Increase timeout
testWidgets(
'long-running test',
(tester) async {
await tester.pumpWidget(MyWidget());
await Future.delayed(const Duration(seconds: 10));
},
timeout: const Timeout(Duration(minutes: 1)),
);
// Solution 2: Fix infinite pumpAndSettle
testWidgets('fix infinite settle', (tester) async {
await tester.pumpWidget(MyWidget());
await tester.pump(); // Use pump() instead of pumpAndSettle() if animation loops
});
```
## Data Errors
### 'RangeError'
**Error Message:**
```
RangeError: Index out of range: index should be less than 5, but is 5
```
**Cause:** Accessing list/array with invalid index.
**Solution:** Check bounds before access or handle empty cases.
```dart
// Problem
testWidgets('range error', (tester) async {
final items = [1, 2, 3, 4, 5];
expect(items[5], 6); // Error: index 5 out of range
});
// Solution 1: Check bounds
testWidgets('safe access', (tester) async {
final items = [1, 2, 3, 4, 5];
if (items.length > 5) {
expect(items[5], 6);
}
});
// Solution 2: Handle empty lists
testWidgets('handle empty list', (tester) async {
final items = <int>[];
if (items.isEmpty) {
expect(find.text('No items'), findsOneWidget);
} else {
expect(items.first, 1);
}
});
```
## Debugging Tips
### Enable Verbose Logging
```bash
flutter test --verbose
```
### Take Screenshots in Integration Tests
```dart
import 'package:integration_test/integration_test.dart';
testWidgets('debug with screenshot', (tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
await tester.pumpWidget(MyWidget());
await binding.convertFlutterSurfaceToImage();
await tester.pump();
await binding.takeScreenshot('debug-state');
});
```
### Print Widget Tree
```dart
testWidgets('print tree', (tester) async {
await tester.pumpWidget(MyWidget());
debugDumpApp();
});
```
### Use Debug Flags
```dart
testWidgets('debug flags', (tester) async {
debugPrint('Current state: ...');
await tester.pumpWidget(MyWidget());
debugPrint('Widget tree: ${tester.widgetList(find.byType(MyWidget))}');
});
```
## Prevention
### Use Type Safety
```dart
// Good: Use type-safe access
final text = tester.widget<Text>(find.byType(Text));
// Avoid: Unsafe casts
final text = find.byType(Text).evaluate().first.widget as Text;
```
### Check Before Actions
```dart
testWidgets('safe tap', (tester) async {
await tester.pumpWidget(MyWidget());
final button = find.byType(ElevatedButton);
if (button.evaluate().isNotEmpty) {
await tester.tap(button);
} else {
fail('Button not found');
}
});
```
### Handle All Cases
```dart
testWidgets('comprehensive', (tester) async {
await tester.pumpWidget(MyWidget());
// Check all possible states
if (find.text('Loading').evaluate().isNotEmpty) {
// Handle loading state
} else if (find.text('Error').evaluate().isNotEmpty) {
// Handle error state
} else {
// Handle success state
}
});
```
@@ -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
```
@@ -0,0 +1,588 @@
# Mocking Guide
## Overview
Mocking replaces real dependencies with test doubles to isolate code and make tests deterministic. This guide covers mocking strategies for Flutter tests.
## When to Mock
- Testing code that depends on external services (APIs, databases)
- Isolating components for unit tests
- Simulating error conditions and edge cases
- Making tests faster and more reliable
- Testing code that uses plugins
## Using Mockito
### Setup
Add to `pubspec.yaml`:
```yaml
dev_dependencies:
build_runner: ^2.10.4
mockito: ^5.6.1
```
### Generate Mocks
Create a test file with a Mockito annotation and import the generated file:
```dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_repository_test.mocks.dart';
@GenerateMocks([ApiClient, UserRepository])
void main() {}
```
Generate mocks:
```bash
dart run build_runner build
```
### Basic Mocking
```dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'my_service_test.mocks.dart';
@GenerateMocks([ApiService])
void main() {
late MockApiService mockApiService;
late MyService myService;
setUp(() {
mockApiService = MockApiService();
myService = MyService(mockApiService);
});
test('calls API and returns data', () async {
// Arrange
when(mockApiService.fetchData())
.thenAnswer((_) async => {'data': 'value'});
// Act
final result = await myService.getData();
// Assert
expect(result['data'], 'value');
verify(mockApiService.fetchData()).called(1);
});
}
```
## Mocking Different Types
### Mocking Classes
```dart
class DataService {
Future<String> fetchData() async => 'real data';
}
// In a real test file, generate MockDataService with @GenerateMocks([DataService]).
test('mocks class method', () async {
final mockService = MockDataService();
when(mockService.fetchData())
.thenAnswer((_) async => 'mocked data');
final result = await mockService.fetchData();
expect(result, 'mocked data');
});
```
### Mocking Interfaces
```dart
abstract class Storage {
void save(String key, String value);
String? load(String key);
}
class FakeStorage implements Storage {
final values = <String, String>{};
@override
void save(String key, String value) {
values[key] = value;
}
@override
String? load(String key) => values[key];
}
// Test
test('uses a fake storage implementation', () {
final storage = FakeStorage();
storage.save('key', 'value');
expect(storage.load('key'), 'value');
});
```
### Mocking Streams
```dart
class StreamService {
Stream<int> get counterStream => StreamController<int>().stream;
}
// Test
test('mocks stream', () async {
final mockService = MockStreamService();
final controller = StreamController<int>();
when(mockService.counterStream).thenAnswer((_) => controller.stream);
expectLater(
mockService.counterStream,
emitsInOrder([1, 2, 3]),
);
controller.add(1);
controller.add(2);
controller.add(3);
await controller.close();
});
```
## Mockito Features
### Returning Values
```dart
when(mockService.getData()).thenReturn('value');
when(mockService.getUser(id)).thenReturn(User(id: id, name: 'Test'));
when(mockService.getList()).thenReturn([1, 2, 3]);
```
### Throwing Errors
```dart
when(mockService.getData())
.thenThrow(Exception('Network error'));
when(mockService.saveData(data))
.thenThrow(FormatException('Invalid data'));
```
### Async Responses
```dart
when(mockService.fetchData())
.thenAnswer((_) async => 'async data');
when(mockService.loadUser(id))
.thenAnswer((_) async => Future.delayed(
const Duration(milliseconds: 100),
() => User(id: id),
));
```
### Sequential Returns
```dart
when(mockService.getData())
.thenReturn('first')
.thenReturn('second')
.thenThrow(Exception('error'));
// First call returns 'first'
expect(await mockService.getData(), 'first');
// Second call returns 'second'
expect(await mockService.getData(), 'second');
// Third call throws
await expectLater(mockService.getData(), throwsException);
```
### Conditional Returns
```dart
when(mockService.getUser(argThat(allOf(isA<String>(), contains('@')))))
.thenReturn(User(email: 'test@example.com'));
when(mockService.getUser(argThat(predicate<String>((arg) => arg.length > 5))))
.thenReturn(User(name: 'Long Name'));
// Matches email format
expect(mockService.getUser('test@example.com').email, 'test@example.com');
// Matches length > 5
expect(mockService.getUser('Long Name').name, 'Long Name');
```
### Capturing Arguments
```dart
mockService.save('key1', 'value1');
mockService.save('key2', 'value2');
final captured = verify(mockService.save(captureAny, captureAny)).captured;
expect(captured, ['key1', 'value1', 'key2', 'value2']);
```
## Verification
### Verify Calls
```dart
mockService.getData();
verify(mockService.getData()).called(1);
verify(mockService.getData()).called(greaterThan(0));
verifyNever(mockService.otherMethod());
```
### Verify In Order
```dart
mockService.first();
mockService.second();
mockService.third();
verifyInOrder([
mockService.first(),
mockService.second(),
mockService.third(),
]);
```
### Verify Specific Arguments
```dart
mockService.save('key', 'value');
verify(mockService.save('key', 'value')).called(1);
verify(mockService.save(argThat(isA<String>()), any)).called(1);
```
### Verify No More Interactions
```dart
mockService.getData();
verify(mockService.getData()).called(1);
verifyNoMoreInteractions(mockService);
verifyNoInteractions(otherMockService);
```
## Mocking Platform Channels
### Basic Platform Channel Mock
```dart
import 'package:flutter/services.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.app/channel'),
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'getBatteryLevel':
return 85;
case 'openUrl':
return true;
default:
throw MissingPluginException();
}
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.app/channel'),
null,
);
});
test('gets battery level', () async {
final platform = MethodChannel('com.example.app/channel');
final result = await platform.invokeMethod('getBatteryLevel');
expect(result, 85);
});
}
```
### Platform Channel with Arguments
```dart
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.app/storage'),
(MethodCall methodCall) async {
if (methodCall.method == 'save') {
final args = methodCall.arguments as Map;
storage[args['key']] = args['value'];
return true;
} else if (methodCall.method == 'load') {
final args = methodCall.arguments as Map;
return storage[args['key']];
}
return null;
},
);
});
test('saves and loads from platform storage', () async {
final platform = MethodChannel('com.example.app/storage');
await platform.invokeMethod('save', {'key': 'test', 'value': 'data'});
final result = await platform.invokeMethod('load', {'key': 'test'});
expect(result, 'data');
});
```
## Mocking Repositories
### Data Repository
```dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_repository_test.mocks.dart';
class UserRepository {
final ApiClient apiClient;
UserRepository(this.apiClient);
Future<User> getUser(String id) async {
final data = await apiClient.get('/users/$id');
return User.fromJson(data);
}
}
@GenerateMocks([ApiClient])
void main() {
late MockApiClient mockApiClient;
late UserRepository userRepository;
setUp(() {
mockApiClient = MockApiClient();
userRepository = UserRepository(mockApiClient);
});
test('fetches and parses user', () async {
when(mockApiClient.get('/users/123'))
.thenAnswer((_) async => {
'id': '123',
'name': 'John Doe',
'email': 'john@example.com',
});
final user = await userRepository.getUser('123');
expect(user.id, '123');
expect(user.name, 'John Doe');
verify(mockApiClient.get('/users/123')).called(1);
});
}
```
## Mocking State Management
### BLoC Mock
```dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<IncrementEvent>((event, emit) => emit(CounterIncremented()));
}
}
// Test
test('emits states on increment', () {
final bloc = CounterBloc();
expectLater(bloc, emitsInOrder([
CounterInitial(),
CounterIncremented(),
]));
bloc.add(IncrementEvent());
});
```
### Provider Mock
```dart
class AuthProvider extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners();
}
}
// Test
test('notifies listeners on login', () {
final provider = AuthProvider();
bool notified = false;
provider.addListener(() {
notified = true;
});
provider.login();
expect(notified, true);
expect(provider.isLoggedIn, true);
});
```
## Manual Mocks
### Simple Manual Mock
```dart
class ApiService {
Future<String> fetchData() async => 'real data';
}
class MockApiService implements ApiService {
String? _mockedResponse;
void setMockResponse(String response) {
_mockedResponse = response;
}
@override
Future<String> fetchData() async => _mockedResponse ?? 'default mock';
}
// Test
test('uses manual mock', () async {
final mockService = MockApiService();
mockService.setMockResponse('test data');
final result = await mockService.fetchData();
expect(result, 'test data');
});
```
### State-Based Mock
```dart
class StatefulMockService implements ApiService {
final List<String> responses = [];
int _callCount = 0;
@override
Future<String> fetchData() async {
if (_callCount < responses.length) {
return responses[_callCount++];
}
throw Exception('No more responses');
}
void addResponse(String response) {
responses.add(response);
}
}
// Test
test('returns sequential responses', () async {
final mock = StatefulMockService();
mock.addResponse('first');
mock.addResponse('second');
expect(await mock.fetchData(), 'first');
expect(await mock.fetchData(), 'second');
await expectLater(mock.fetchData(), throwsException);
});
```
## Best Practices
1. **Mock at boundaries** - Mock external dependencies, not internal logic
2. **Avoid over-mocking** - Only mock what's necessary for the test
3. **Verify interactions** - Ensure mocks are called correctly
4. **Use specific matchers** - Avoid `any` when possible
5. **Clear expectations** - Make test intentions clear
6. **Reset mocks** - Use setUp/tearDown to clean up
7. **Test real behavior** - Verify expected outcomes, not implementation
8. **Keep mocks simple** - Complex mocks indicate design issues
## Common Patterns
### Mock Chain
```dart
when(mockService.first())
.thenReturn(mockSecondService);
when(mockSecondService.getData())
.thenReturn('result');
final result = mockService.first().getData();
expect(result, 'result');
```
### Mock Repository
```dart
class MockRepository {
final Map<String, dynamic> _data = {};
T get<T>(String key) => _data[key] as T;
void set<T>(String key, T value) => _data[key] = value;
}
// Test
test('uses mock repository', () {
final repo = MockRepository();
repo.set('name', 'John');
expect(repo.get<String>('name'), 'John');
});
```
### Mock Factory
```dart
class ServiceFactory {
static ApiService create() => RealApiService();
}
class MockServiceFactory {
ApiService? mockService;
ApiService create() => mockService ?? RealApiService();
}
// Test
test('uses factory mock', () {
final factory = MockServiceFactory();
factory.mockService = MockApiService();
when(factory.mockService!.getData()).thenReturn('mocked');
final service = factory.create();
expect(service.getData(), 'mocked');
});
```
@@ -0,0 +1,493 @@
# Plugin Testing Guide
## Overview
Flutter plugins require special testing strategies because they include native code. This guide covers testing plugin packages with Dart, native, and integration tests.
## Test Types for Plugins
| Type | Tests | Runs On | Purpose |
|----------------|---------------------------|------------------|-----------------------------------|
| Dart Unit | Single classes/functions | Dart VM | Test Dart code in isolation |
| Dart Widget | UI components | Test environment | Test widget behavior |
| Dart Integration | Dart + Native bridge | Device/Emulator | Test full plugin functionality |
| Native Unit | Native code | Native test env | Test native code in isolation |
| Native UI | Native UI + Flutter UI | Device/Emulator | Test native UI interactions |
## Project Structure
```
my_plugin/
├── lib/ # Dart code
│ └── my_plugin.dart
├── android/ # Android native code
│ └── src/test/ # Android unit tests
├── ios/ # iOS native code
├── example/ # Example app
│ ├── integration_test/ # Integration tests
│ ├── ios/RunnerTests/ # iOS tests
│ └── lib/
├── test/ # Dart unit tests
│ └── my_plugin_test.dart
└── pubspec.yaml
```
## Dart Unit Tests
### Basic Plugin Unit Test
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_plugin/my_plugin.dart';
void main() {
test('plugin initializes correctly', () {
final plugin = MyPlugin();
expect(plugin.initialized, false);
plugin.initialize();
expect(plugin.initialized, true);
});
test('processes data correctly', () {
final plugin = MyPlugin();
final result = plugin.processData([1, 2, 3]);
expect(result, [2, 4, 6]);
});
}
```
### Mocking Platform Channels in Unit Tests
```dart
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_plugin/my_plugin.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late MyPlugin plugin;
setUp(() {
// Mock platform channel
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
(MethodCall methodCall) async {
if (methodCall.method == 'getNativeValue') {
return 42;
}
return null;
},
);
plugin = MyPlugin();
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
null,
);
});
test('gets value from native side', () async {
final result = await plugin.getNativeValue();
expect(result, 42);
});
}
```
## Dart Integration Tests
### Setup
Add to `example/pubspec.yaml`:
```yaml
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
my_plugin:
path: ../
```
### Basic Integration Test
```dart
// example/integration_test/my_plugin_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_plugin_example/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('plugin works in real app', (tester) async {
await tester.pumpWidget(const MyApp());
final plugin = MyPlugin();
final result = await plugin.getNativeValue();
expect(result, greaterThan(0));
});
}
```
### Create Test Driver
```dart
// test_driver/integration_test.dart
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
```
### Run Integration Tests
```bash
# From example directory
cd example
flutter test integration_test/
# Or with driver
flutter drive --target=integration_test/my_plugin_test.dart
```
## Android Unit Tests
### Setup
Create test in `android/src/test/java/com/example/my_plugin/MyPluginTest.java`:
```java
package com.example.my_plugin;
import org.junit.Test;
import static org.junit.Assert.*;
public class MyPluginTest {
@Test
public void testNativeMethod() {
MyPlugin plugin = new MyPlugin();
int result = plugin.calculate(10, 20);
assertEquals(30, result);
}
@Test
public void testStringProcessing() {
MyPlugin plugin = new MyPlugin();
String result = plugin.process("hello");
assertEquals("HELLO", result);
}
}
```
### Run Android Tests
```bash
# From android directory
cd example/android
# Run all tests
./gradlew testDebugUnitTest
# Run specific test
./gradlew test --tests MyPluginTest.testNativeMethod
```
## iOS Unit Tests
### Setup
Create test in `example/ios/RunnerTests/RunnerTests.m`:
```objectivec
#import <XCTest/XCTest.h>
#import "MyPlugin.h"
@interface MyPluginTest : XCTestCase
@end
@implementation MyPluginTest
- (void)setUp {
[super setUp];
// Setup code
}
- (void)testNativeMethod {
MyPlugin *plugin = [[MyPlugin alloc] init];
NSInteger result = [plugin calculateWithA:10 b:20];
XCTAssertEqual(result, 30);
}
- (void)testStringProcessing {
MyPlugin *plugin = [[MyPlugin alloc] init];
NSString *result = [plugin processString:@"hello"];
XCTAssertEqualObjects(result, @"HELLO");
}
@end
```
### Run iOS Tests
```bash
# From ios directory
cd example/ios
# Run tests
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug
# Or run from Xcode
# Product > Test
```
## Native UI Tests (Espresso/XCUITest)
### Android Espresso Test
```java
// android/src/androidTest/java/com/example/my_plugin/UiTest.java
package com.example.my_plugin;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class UiTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void testPluginButton() {
onView(withText("Plugin Button"))
.check(matches(isDisplayed()));
}
}
```
### iOS XCUITest
```objectivec
// example/ios/RunnerUITests/RunnerUITests.m
#import <XCTest/XCTest.h>
@interface RunnerUITests : XCTestCase
@end
@implementation RunnerUITests
- (void)setUp {
[super setUp];
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
}
- (void)testPluginButtonExists {
XCUIApplication *app = [[XCUIApplication alloc] init];
XCUIElement *button = app.buttons[@"Plugin Button"];
XCTAssertTrue(button.exists);
}
- (void)testPluginButtonClick {
XCUIApplication *app = [[XCUIApplication alloc] init];
XCUIElement *button = app.buttons[@"Plugin Button"];
[button tap];
XCUIElement *result = app.staticTexts[@"Success"];
XCTAssertTrue(result.exists);
}
@end
```
## Testing Plugin Initialization
### Test Plugin Registration
```dart
testWidgets('plugin registers correctly', (tester) async {
final calls = <MethodCall>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
(MethodCall methodCall) async {
calls.add(methodCall);
return null;
},
);
await tester.pumpWidget(const MyApp());
expect(calls.any((call) => call.method == 'initialize'), true);
});
```
## Testing Error Handling
### Native Error Handling
```dart
testWidgets('handles native errors', (tester) async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
(MethodCall methodCall) async {
if (methodCall.method == 'failingMethod') {
throw PlatformException(
code: 'ERROR',
message: 'Native error occurred',
);
}
return null;
},
);
final plugin = MyPlugin();
expect(
() => plugin.failingMethod(),
throwsA(isA<PlatformException>()),
);
});
```
## Testing Platform-Specific Features
### Android-Specific Tests
```java
@Test
public void testAndroidOnlyFeature() {
MyPlugin plugin = new MyPlugin();
// Test Android-specific API
Context context = InstrumentationRegistry.getTargetContext();
String deviceModel = plugin.getDeviceModel(context);
assertNotNull(deviceModel);
}
```
### iOS-Specific Tests
```objectivec
- (void)testIOSOnlyFeature {
MyPlugin *plugin = [[MyPlugin alloc] init];
// Test iOS-specific API
NSString *deviceName = plugin.getDeviceName();
XCTAssertNotNil(deviceName);
}
```
## Testing Multiple Platforms
### Platform Detection Test
```dart
testWidgets('works on all platforms', (tester) async {
final plugin = MyPlugin();
final platform = await plugin.getPlatform();
if (Platform.isAndroid) {
expect(platform, contains('Android'));
} else if (Platform.isIOS) {
expect(platform, contains('iOS'));
}
});
```
## Testing Performance
### Native Performance Test
```java
@Test
public void testPerformance() {
MyPlugin plugin = new MyPlugin();
long startTime = System.nanoTime();
plugin.heavyOperation();
long endTime = System.nanoTime();
long duration = endTime - startTime;
long maxDuration = 100_000_000; // 100ms
assertTrue("Operation too slow", duration < maxDuration);
}
```
## Best Practices
1. **Test at each layer** - Dart, native, and integration
2. **Mock platform channels** - In Dart unit tests
3. **Test error cases** - Native failures, network issues
4. **Use integration tests** - Test full plugin functionality
5. **Test on real devices** - For platform-specific features
6. **Keep tests isolated** - Each test should be independent
7. **Test all platforms** - Android, iOS, web, desktop
8. **Document platform limitations** - Note platform-specific behavior
## CI/CD for Plugins
### GitHub Actions
```yaml
name: Plugin Tests
on: [push, pull_request]
jobs:
dart-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
- run: flutter test
android-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run Android tests
run: |
cd example/android
./gradlew testDebugUnitTest
ios-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run iOS tests
run: |
cd example/ios
xcodebuild test -workspace Runner.xcworkspace \
-scheme Runner -configuration Debug
```
@@ -0,0 +1,433 @@
# Unit Testing Guide
## Overview
Unit tests test individual functions, methods, or classes in isolation. They are the foundation of a well-tested Flutter app, providing fast feedback and high maintainability.
## When to Write Unit Tests
- Testing business logic functions
- Validating data transformations
- Testing state management logic (Bloc, Provider, Riverpod)
- Mocking external services/API calls
- Testing utility functions and helpers
## Test Structure
### Basic Test
```dart
import 'package:test/test.dart';
void main() {
test('description', () {
// Arrange
final subject = MyClass();
// Act
final result = subject.myMethod();
// Assert
expect(result, expectedValue);
});
}
```
### Group Tests
```dart
import 'package:test/test.dart';
void main() {
group('Counter', () {
late Counter counter;
setUp(() {
counter = Counter();
});
test('value starts at 0', () {
expect(counter.value, 0);
});
test('increment increases value', () {
counter.increment();
expect(counter.value, 1);
});
tearDown(() {
counter.dispose();
});
});
}
```
## Testing Patterns
### Testing Pure Functions
```dart
// Function to test
int add(int a, int b) => a + b;
// Test
test('add returns sum of two numbers', () {
expect(add(2, 3), 5);
expect(add(-1, 1), 0);
});
```
### Testing State Changes
```dart
class Counter {
int _value = 0;
int get value => _value;
void increment() => _value++;
void decrement() => _value--;
void reset() => _value = 0;
}
// Tests
group('Counter state changes', () {
late Counter counter;
setUp(() => counter = Counter());
test('initial value is 0', () {
expect(counter.value, 0);
});
test('increment increases value by 1', () {
counter.increment();
expect(counter.value, 1);
});
test('decrement decreases value by 1', () {
counter.decrement();
expect(counter.value, -1);
});
test('multiple increments work correctly', () {
counter.increment();
counter.increment();
counter.increment();
expect(counter.value, 3);
});
});
```
### Testing Async Operations
```dart
class DataService {
Future<String> fetchData() async {
await Future.delayed(const Duration(seconds: 1));
return 'data';
}
}
// Test
test('fetchData returns data after delay', () async {
final service = DataService();
final result = await service.fetchData();
expect(result, 'data');
});
```
### Testing Streams
```dart
class CounterStream {
final _controller = StreamController<int>();
Stream<int> get stream => _controller.stream;
void increment() => _controller.sink.add(1);
void dispose() => _controller.close();
}
// Test
test('stream emits values', () async {
final counter = CounterStream();
expectLater(
counter.stream,
emitsInOrder([1, 1, 1]),
);
counter.increment();
counter.increment();
counter.increment();
await Future.delayed(const Duration(milliseconds: 100));
counter.dispose();
});
```
## Matchers
### Common Matchers
```dart
// Equality
expect(actual, equals(expected));
// Not equal
expect(actual, isNot(equals(expected)));
// Null checks
expect(value, isNull);
expect(value, isNotNull);
// Numeric comparisons
expect(5, greaterThan(3));
expect(5, lessThan(10));
expect(5, greaterThanOrEqualTo(5));
expect(5, lessThanOrEqualTo(5));
// Type checking
expect(obj, isA<String>());
expect(obj, isNot(isA<int>()));
// String matching
expect(text, contains('substring'));
expect(text, startsWith('prefix'));
expect(text, endsWith('suffix'));
expect(text, matches(RegExp(r'\d+')));
// Lists
expect(list, isEmpty);
expect(list, isNotEmpty);
expect(list, hasLength(3));
expect(list, contains(item));
expect(list, orderedEquals([1, 2, 3]));
```
### Custom Matchers
```dart
import 'package:test/test.dart';
class HasLength extends Matcher {
final int expectedLength;
HasLength(this.expectedLength);
@override
bool matches(item, Map matchState) {
return (item as List).length == expectedLength;
}
@override
Description describe(Description description) {
return description.add('has length $expectedLength');
}
}
// Usage
test('list has custom length', () {
final list = [1, 2, 3];
expect(list, HasLength(3));
});
```
## Exception Testing
```dart
class Calculator {
int divide(int a, int b) {
if (b == 0) throw ArgumentError('Cannot divide by zero');
return a ~/ b;
}
}
// Test
test('divide throws when dividing by zero', () {
final calculator = Calculator();
expect(
() => calculator.divide(10, 0),
throwsArgumentError,
);
});
test('divide throws with specific message', () {
final calculator = Calculator();
expect(
() => calculator.divide(10, 0),
throwsA(isA<ArgumentError>()
.having((e) => e.message, 'message', contains('zero'))),
);
});
```
## Mocking
### Using Mockito
```dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_service_test.mocks.dart';
@GenerateMocks([ApiClient])
void main() {
group('UserService', () {
late MockApiClient mockApiClient;
late UserService userService;
setUp(() {
mockApiClient = MockApiClient();
userService = UserService(mockApiClient);
});
test('fetches user data', () async {
when(mockApiClient.getUser('123'))
.thenAnswer((_) async => User(id: '123', name: 'John'));
final user = await userService.getUser('123');
expect(user.name, 'John');
verify(mockApiClient.getUser('123')).called(1);
});
});
}
```
Generate the mock file with:
```bash
dart run build_runner build
```
### Manual Mocking
```dart
class MockStorage implements Storage {
String? savedData;
@override
void save(String data) {
savedData = data;
}
@override
String? load() => savedData;
}
// Test
test('saves and loads data', () {
final mockStorage = MockStorage();
final service = DataService(mockStorage);
service.saveData('test');
expect(mockStorage.savedData, 'test');
final loaded = service.loadData();
expect(loaded, 'test');
});
```
## Testing with Fake Clock
```dart
import 'package:clock/clock.dart';
test('time-based operations', () {
withClock(Clock(() => DateTime(2024, 1, 1)), () {
final timestamp = DateTime.now();
expect(timestamp.year, 2024);
});
});
```
## Test Organization
### File Structure
```
lib/
counter.dart
test/
counter/
counter_test.dart
counter_value_test.dart
counter_operations_test.dart
```
### Naming Conventions
- Test files: `*_test.dart`
- Test groups: Group related functionality
- Test names: Describe what and why
```dart
// Good
test('value increments when increment is called', () {});
// Avoid
test('increment', () {});
```
## Best Practices
1. **Arrange-Act-Assert** - Structure tests with clear sections
2. **One assertion per test** - When possible, keep tests focused
3. **Descriptive names** - Explain what and why, not just what
4. **Avoid test interdependence** - Each test should be independent
5. **Use setUp/tearDown** - Avoid code duplication
6. **Test edge cases** - Test boundaries, nulls, and errors
7. **Keep tests fast** - Avoid slow operations in unit tests
8. **Mock external dependencies** - Tests should be deterministic
## Common Pitfalls
### Testing Implementation Details
```dart
// Bad - tests implementation
test('counter calls _update', () {
verify(counter._update()).called(1);
});
// Good - tests behavior
test('counter value increases after increment', () {
expect(counter.value, 1);
});
```
### Fragile Tests
```dart
// Bad - depends on exact timing
test('completes within 100ms', () async {
final stopwatch = Stopwatch()..start();
await operation();
expect(stopwatch.elapsedMilliseconds, lessThan(100));
});
// Good - tests completion
test('completes successfully', () async {
await operation();
// operation completed without error
});
```
## Running Tests
```bash
# Run all tests
flutter test
# Run specific file
flutter test test/counter/counter_test.dart
# Run with coverage
flutter test --coverage
# Run verbose
flutter test --verbose
# Run specific test
flutter test --name "value increments"
```
@@ -0,0 +1,670 @@
# Widget Testing Guide
## Overview
Widget tests (component tests) verify that widgets render correctly and interact as expected. They run in a test environment that simulates widget lifecycle without a full UI system.
## When to Write Widget Tests
- Verifying widget rendering
- Testing user interactions (taps, drags, scrolling)
- Validating widget state changes
- Testing different orientations and screen sizes
- Testing form inputs and validation
## Test Structure
### Basic Widget Test
```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('widget description', (tester) async {
// Build widget
await tester.pumpWidget(const MyWidget());
// Find and verify
expect(find.text('Hello'), findsOneWidget);
});
}
```
### Using MaterialApp
```dart
testWidgets('widget with MaterialApp', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MyWidget(),
),
),
);
expect(find.byType(MyWidget), findsOneWidget);
});
```
## Finding Widgets
### Common Finders
```dart
// By text
find.text('Hello')
find.textContaining('ello')
find.textRegExp(RegExp(r'\d+'))
// By widget type
find.byType(Text)
find.byType(ElevatedButton)
find.byType(MyWidget)
// By key
find.byKey(const Key('my-button'))
find.byKey(const ValueKey('counter'))
// By icon
find.byIcon(Icons.add)
// By widget instance
find.byWidget(myWidgetInstance)
// By specific widget ancestor
find.ancestor(
of: find.text('Child'),
matching: find.byType(Column),
)
```
### Finder Matchers
```dart
// Find exactly one
expect(find.text('Hello'), findsOneWidget);
// Find multiple
expect(find.text('Hello'), findsWidgets);
// Find nothing
expect(find.text('Missing'), findsNothing);
// Find at least N
expect(find.text('Item'), findsNWidgets(3));
```
## User Interactions
### Tapping
```dart
// Tap a widget
await tester.tap(find.byType(ElevatedButton));
// Trigger a frame
await tester.pump();
// Wait for all animations
await tester.pumpAndSettle();
```
### Dragging
```dart
// Drag a widget
await tester.drag(
find.byType(MyWidget),
const Offset(0, -300),
);
await tester.pumpAndSettle();
```
### Entering Text
```dart
// Enter text into TextField
await tester.enterText(
find.byType(TextField),
'Hello World',
);
// Tap to focus, then enter
await tester.tap(find.byType(TextField));
await tester.enterText(find.byType(TextField), 'Text');
// Enter by key
await tester.enterText(
find.byKey(const Key('email-field')),
'test@example.com',
);
```
### Scrolling
```dart
// Scroll a ListView
await tester.fling(
find.byType(ListView),
const Offset(0, -500),
10000,
);
await tester.pumpAndSettle();
// Scroll until widget is visible
await tester.scrollUntilVisible(
find.text('Target Item'),
500.0,
);
```
### Long Press
```dart
await tester.longPress(find.byType(IconButton));
await tester.pumpAndSettle();
```
## Testing Widget Properties
### Text Content
```dart
testWidgets('displays correct text', (tester) async {
await tester.pumpWidget(const MyWidget(text: 'Hello'));
expect(find.text('Hello'), findsOneWidget);
expect(find.text('Goodbye'), findsNothing);
});
```
### Widget State
```dart
testWidgets('updates state on button press', (tester) async {
await tester.pumpWidget(const MyApp());
// Initial state
expect(find.text('Count: 0'), findsOneWidget);
// Tap increment button
await tester.tap(find.byKey(const Key('increment')));
await tester.pumpAndSettle();
// Updated state
expect(find.text('Count: 1'), findsOneWidget);
});
```
### Conditional Rendering
```dart
testWidgets('shows loading indicator when loading', (tester) async {
await tester.pumpWidget(MyWidget(isLoading: true));
expect(find.byType(CircularProgressIndicator), findsOneWidget);
expect(find.byType(ElevatedButton), findsNothing);
});
testWidgets('shows button when not loading', (tester) async {
await tester.pumpWidget(MyWidget(isLoading: false));
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.byType(ElevatedButton), findsOneWidget);
});
```
## Testing with Arguments
```dart
testWidgets('receives and displays title', (tester) async {
const title = 'My Title';
await tester.pumpWidget(
MaterialApp(
home: MyWidget(title: title),
),
);
expect(find.text(title), findsOneWidget);
});
testWidgets('receives and uses callback', (tester) async {
bool callbackCalled = false;
await tester.pumpWidget(
MaterialApp(
home: MyWidget(
onPressed: () => callbackCalled = true,
),
),
);
await tester.tap(find.byType(ElevatedButton));
expect(callbackCalled, true);
});
```
## Testing Orientation
```dart
testWidgets('widget in portrait mode', (tester) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
await tester.pumpWidget(const MyApp());
// Verify portrait layout
expect(find.byType(Column), findsOneWidget);
// Clean up
addTearDown(tester.binding.setSurfaceSize(null));
});
testWidgets('widget in landscape mode', (tester) async {
await tester.binding.setSurfaceSize(const Size(800, 400));
await tester.pumpWidget(const MyApp());
// Verify landscape layout
expect(find.byType(Row), findsOneWidget);
// Clean up
addTearDown(tester.binding.setSurfaceSize(null));
});
```
## Testing Scrollable Widgets
### ListView Scrolling
```dart
testWidgets('scrolling in ListView', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
),
),
);
// Initial item visible
expect(find.text('Item 0'), findsOneWidget);
expect(find.text('Item 50'), findsNothing);
// Scroll down
await tester.fling(
find.byType(ListView),
const Offset(0, -5000),
10000,
);
await tester.pumpAndSettle();
// Later item now visible
expect(find.text('Item 50'), findsOneWidget);
});
```
### Scroll Until Visible
```dart
testWidgets('scroll until specific item is visible', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(
key: ValueKey('item-$index'),
title: Text('Item $index'),
),
),
),
);
// Scroll until item 75 is visible
await tester.scrollUntilVisible(
find.byKey(const ValueKey('item-75')),
500.0,
);
expect(find.text('Item 75'), findsOneWidget);
});
```
## Testing Forms
### TextField Validation
```dart
testWidgets('validates email field', (tester) async {
final formKey = GlobalKey<FormState>();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Form(
key: formKey,
child: Column(
children: [
TextFormField(
key: const Key('email'),
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Invalid email';
}
return null;
},
),
ElevatedButton(
key: const Key('submit'),
onPressed: () => formKey.currentState!.validate(),
child: const Text('Submit'),
),
],
),
),
),
),
);
// Enter invalid email
await tester.enterText(find.byKey(const Key('email')), 'invalid');
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
// Should show error
expect(find.text('Invalid email'), findsOneWidget);
// Enter valid email
await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
await tester.tap(find.byKey(const Key('submit')));
await tester.pump();
// Should hide error
expect(find.text('Invalid email'), findsNothing);
});
```
### Multiple Form Fields
```dart
testWidgets('form with multiple fields', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
key: const Key('name'),
decoration: const InputDecoration(labelText: 'Name'),
),
TextFormField(
key: const Key('email'),
decoration: const InputDecoration(labelText: 'Email'),
),
ElevatedButton(
key: const Key('submit'),
onPressed: () {},
child: const Text('Submit'),
),
],
),
),
),
),
);
// Fill form
await tester.enterText(find.byKey(const Key('name')), 'John Doe');
await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
// Submit
await tester.tap(find.byKey(const Key('submit')));
await tester.pumpAndSettle();
});
```
## Testing Animations
```dart
testWidgets('animation plays correctly', (tester) async {
await tester.pumpWidget(const AnimatedWidget());
// Initial state
expect(find.byType(Opacity), findsOneWidget);
// Trigger animation
await tester.tap(find.byType(ElevatedButton));
// Pump animation frames
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
// Animation complete
await tester.pumpAndSettle();
// Verify final state
final opacity = tester.widget<Opacity>(find.byType(Opacity));
expect(opacity.opacity, 0.0);
});
```
## Testing Custom Widgets
### Testing InheritedWidgets
```dart
class MyInheritedWidget extends InheritedWidget {
final String data;
const MyInheritedWidget({
super.key,
required this.data,
required Widget child,
}) : super(child: child);
static MyInheritedWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!;
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) =>
data != oldWidget.data;
}
testWidgets('inherited widget provides data', (tester) async {
await tester.pumpWidget(
MyInheritedWidget(
data: 'Test Data',
child: Builder(
builder: (context) {
return Text(MyInheritedWidget.of(context).data);
},
),
),
);
expect(find.text('Test Data'), findsOneWidget);
});
```
### Testing StatefulWidget
```dart
class CounterWidget extends StatefulWidget {
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void _increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
key: const Key('increment'),
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
testWidgets('CounterWidget increments correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CounterWidget(),
),
),
);
// Initial count
expect(find.text('Count: 0'), findsOneWidget);
// Increment
await tester.tap(find.byKey(const Key('increment')));
await tester.pump();
expect(find.text('Count: 1'), findsOneWidget);
});
```
## Testing with Mock Data
```dart
class ProductList extends StatelessWidget {
final List<Product> products;
const ProductList({super.key, required this.products});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ListTile(
title: Text(products[index].name),
subtitle: Text('\$${products[index].price}'),
),
);
}
}
testWidgets('displays list of products', (tester) async {
final products = [
Product(name: 'Product 1', price: 10.99),
Product(name: 'Product 2', price: 20.99),
];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductList(products: products),
),
),
);
expect(find.text('Product 1'), findsOneWidget);
expect(find.text('\$10.99'), findsOneWidget);
expect(find.text('Product 2'), findsOneWidget);
expect(find.text('\$20.99'), findsOneWidget);
});
```
## Testing Accessibility
```dart
testWidgets('widget has proper semantics', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Semantics(
label: 'Submit button',
button: true,
child: ElevatedButton(
onPressed: () {},
child: const Text('Submit'),
),
),
),
);
final semantics = tester.getSemantics(find.byType(ElevatedButton));
expect(semantics.label, 'Submit button');
expect(semantics.hasAction(SemanticsAction.tap), true);
});
```
## Best Practices
1. **Use descriptive test names** - Explain what and why
2. **Keep tests independent** - Each test should be standalone
3. **Use pumpAndSettle()** - Wait for all animations
4. **Test user behavior, not implementation** - Focus on what users see and do
5. **Group related tests** - Use `group()` for organization
6. **Test edge cases** - Empty lists, null values, errors
7. **Use keys for widgets** - Makes finding easier and more reliable
8. **Test accessibility** - Verify semantic labels and actions
## Common Patterns
### Testing Navigation
```dart
testWidgets('navigates to detail screen', (tester) async {
await tester.pumpWidget(
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
},
),
);
// Tap navigation button
await tester.tap(find.text('Go to Detail'));
await tester.pumpAndSettle();
// Verify navigation
expect(find.text('Detail Screen'), findsOneWidget);
expect(find.text('Home Screen'), findsNothing);
});
```
### Testing Loading States
```dart
testWidgets('shows loading then content', (tester) async {
bool isLoading = true;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (context, setState) {
return isLoading
? const CircularProgressIndicator()
: const Text('Content Loaded');
},
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Simulate data load
isLoading = false;
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('Content Loaded'), findsOneWidget);
});
```