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,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"
```