chore: bring flutter client, docs, deploy and tools into monorepo

This commit is contained in:
2026-07-05 03:22:24 +02:00
parent fcc81e2a74
commit 7a931d2d6f
189 changed files with 19124 additions and 0 deletions
@@ -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');
});
```