Files
Musify/musify_flutter/.agents/skills/flutter-testing/references/mocking.md
T

12 KiB

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:

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:

import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'user_repository_test.mocks.dart';

@GenerateMocks([ApiClient, UserRepository])
void main() {}

Generate mocks:

dart run build_runner build

Basic Mocking

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

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

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

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

when(mockService.getData()).thenReturn('value');

when(mockService.getUser(id)).thenReturn(User(id: id, name: 'Test'));

when(mockService.getList()).thenReturn([1, 2, 3]);

Throwing Errors

when(mockService.getData())
  .thenThrow(Exception('Network error'));

when(mockService.saveData(data))
  .thenThrow(FormatException('Invalid data'));

Async Responses

when(mockService.fetchData())
  .thenAnswer((_) async => 'async data');

when(mockService.loadUser(id))
  .thenAnswer((_) async => Future.delayed(
    const Duration(milliseconds: 100),
    () => User(id: id),
  ));

Sequential Returns

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

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

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

mockService.getData();

verify(mockService.getData()).called(1);
verify(mockService.getData()).called(greaterThan(0));
verifyNever(mockService.otherMethod());

Verify In Order

mockService.first();
mockService.second();
mockService.third();

verifyInOrder([
  mockService.first(),
  mockService.second(),
  mockService.third(),
]);

Verify Specific Arguments

mockService.save('key', 'value');

verify(mockService.save('key', 'value')).called(1);
verify(mockService.save(argThat(isA<String>()), any)).called(1);

Verify No More Interactions

mockService.getData();

verify(mockService.getData()).called(1);
verifyNoMoreInteractions(mockService);

verifyNoInteractions(otherMockService);

Mocking Platform Channels

Basic Platform Channel Mock

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

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

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

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

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

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

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

when(mockService.first())
  .thenReturn(mockSecondService);
when(mockSecondService.getData())
  .thenReturn('result');

final result = mockService.first().getData();
expect(result, 'result');

Mock Repository

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

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');
});