# 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 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 = {}; @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 get counterStream => StreamController().stream; } // Test test('mocks stream', () async { final mockService = MockStreamService(); final controller = StreamController(); 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(), contains('@'))))) .thenReturn(User(email: 'test@example.com')); when(mockService.getUser(argThat(predicate((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()), 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 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 { CounterBloc() : super(CounterInitial()) { on((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 fetchData() async => 'real data'; } class MockApiService implements ApiService { String? _mockedResponse; void setMockResponse(String response) { _mockedResponse = response; } @override Future 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 responses = []; int _callCount = 0; @override Future 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 _data = {}; T get(String key) => _data[key] as T; void set(String key, T value) => _data[key] = value; } // Test test('uses mock repository', () { final repo = MockRepository(); repo.set('name', 'John'); expect(repo.get('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'); }); ```