Mocking Dependencies
4 min read
Testing
| Boundary | Mock |
|---|---|
| Repository / data source | ✅ Yes — substitute fake responses |
| API client (HTTP, GraphQL) | ✅ Yes |
| Platform channel handlers | ✅ Yes (channel.setMockMethodCallHandler) |
| Database adapters | ✅ In-memory implementation usually best |
| ViewModel / BLoC / Notifier | ❌ Don't — TEST these |
| Pure functions / utility classes | ❌ Don't — test them directly |
| Concrete UI widgets | ❌ Don't — render them in widget tests |
Rule: mock at module boundaries, test what's inside.
Approach 1: Interface + Mockito (codegen mocks)
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> updateUser(User user);
}
// In a test file
@GenerateMocks([UserRepository])
import 'user_test.mocks.dart';
void main() {
late MockUserRepository repo;
late UserBloc bloc;
setUp(() {
repo = MockUserRepository();
bloc = UserBloc(repo);
});
test('loads user successfully', () async {
when(repo.getUser('1')).thenAnswer((_) async => User(id: '1', name: 'Alice'));
bloc.add(LoadUser('1'));
await expectLater(bloc.stream, emitsInOrder([
isA<UserLoading>(),
isA<UserLoaded>().having((s) => s.user.name, 'name', 'Alice'),
]));
verify(repo.getUser('1')).called(1);
});
}
Strength: rich verifications (verify, verifyNever, argument captures). Weakness: generated code, brittle to interface changes.
Approach 2: Hand-rolled fakes (often cleaner)
class FakeUserRepository implements UserRepository {
final Map<String, User> _users = {};
void seed(User u) => _users[u.id] = u;
@override
Future<User> getUser(String id) async {
final u = _users[id];
if (u == null) throw NotFoundException(id);
return u;
}
@override
Future<void> updateUser(User u) async => _users[u.id] = u;
}
// Test
final repo = FakeUserRepository()..seed(User(id: '1', name: 'Alice'));
final bloc = UserBloc(repo);
// run scenarios
Strength: realistic behaviour, easy to reason about. Weakness: you implement it (some boilerplate for big interfaces).
Approach 3: Riverpod overrides — no mock classes needed
test('dashboard loads', () async {
final container = ProviderContainer(overrides: [
apiClientProvider.overrideWithValue(FakeApiClient()),
userProfileProvider.overrideWith((ref) async =>
UserProfile(id: '1', name: 'Test')),
]);
addTearDown(container.dispose);
final data = await container.read(dashboardProvider.future);
expect(data.profile.name, 'Test');
});
Strength: tests the provider graph end-to-end with no widget pump. Weakness: less suitable when you need verifications.
When to use which approach
| Goal | Approach |
|---|---|
| Need to assert call count / argument captures | Mockito |
| Stable interface, want realistic stateful behavior | Hand-rolled fake |
| Testing Riverpod provider graphs | ProviderContainer overrides |
| Mocking HTTP at the wire level | MockClient from http, or DioAdapter |
| Mocking platform channels | channel.setMockMethodCallHandler |
| Testing widgets that use mocks | Inject via Provider / ProviderScope override / DI |
Common mistakes to avoid
❌ Mocking too many layers
Mock ApiClient AND Repository AND Service → test asserts the test's own
stub behavior, not real code.
✅ Mock at ONE boundary; test the real code above and below.
❌ Mocking concrete classes (Mockito requires abstract for null safety)
Tightly coupled tests; refactor pain.
✅ Code against an abstract interface, mock the interface.
❌ Forgetting verify after when
when() sets up a stub. verify() asserts it was called. If you only stub,
you're not testing the interaction.
❌ Stubbing every method, including ones the test doesn't care about
Noise. Mockito's null-safe mocks auto-return zero values; only stub what matters.
❌ Tests that pass when production code is broken
If your test only tests the mock, it's not testing your system.
Walk the test back: what real bug would this catch?
❌ ProviderContainer leaks between tests
Always container.dispose() (or use addTearDown).
Interview follow-ups
-
When would you prefer a hand-rolled fake over Mockito? When the interface is stable, when you need realistic stateful behavior (a fake DB that actually stores values), or when you want tests that read like English without
when().thenAnswer(...)setup noise. Mockito wins when you need rich verification of how a collaborator was called. -
How do you test code that uses platform channels?
_channel.setMockMethodCallHandler((call) async { return ...; })in test setUp. Your Dart code calls the channel as normal; the test handler returns canned data. Works forMethodChannelandEventChannel. Reset in tearDown to avoid bleeding into other tests. -
What's wrong with mocking a class your test owns (e.g., a value class)? Value classes shouldn't have behavior to mock — they're just data. Mocking them obscures intent and breaks the moment you add a field. Pass real instances; reserve mocks for collaborators with side effects.
-
How do you test a
Stream/Bloc? For Bloc, usebloc_test'sblocTest(...)which encapsulatessetUp/act/expect. For raw streams,expectLater(stream, emitsInOrder([...]))oremitsThroughmatchers. Always verify the sequence of emitted states, not just the final value — that's what catches missing intermediate states like Loading.
How helpful was this content?
Please sign in to rate this article.