Mocking Dependencies

High PriorityAsked in ~70% of senior interviews

4 min read

Testing

BoundaryMock
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

GoalApproach
Need to assert call count / argument capturesMockito
Stable interface, want realistic stateful behaviorHand-rolled fake
Testing Riverpod provider graphsProviderContainer overrides
Mocking HTTP at the wire levelMockClient from http, or DioAdapter
Mocking platform channelschannel.setMockMethodCallHandler
Testing widgets that use mocksInject 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

  1. 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.

  2. 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 for MethodChannel and EventChannel. Reset in tearDown to avoid bleeding into other tests.

  3. 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.

  4. How do you test a Stream/Bloc? For Bloc, use bloc_test's blocTest(...) which encapsulates setUp/act/expect. For raw streams, expectLater(stream, emitsInOrder([...])) or emitsThrough matchers. 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.