# 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 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(); Stream 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()); expect(obj, isNot(isA())); // 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() .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" ```