Flutter Interview Handbook

Mocking & Testability

100%

Mocking & Testability

Writing testable Flutter code requires adhering to SOLID design principles, particularly the Dependency Inversion Principle (DIP). By injecting abstract dependencies into classes rather than instantiating concrete implementations inline, developers can substitute real network and database calls with Mocks, Fakes, and Stubs using mocktail or mockito.


1. Mocks vs. Fakes vs. Stubs vs. Spies

Test DoublePurposeBehaviorExample
StubPre-programmed ResponseReturns fixed data when calledwhen(() => repo.getUser()).thenReturn(user)
MockInteraction VerificationVerifies if methods were called with specific argsverify(() => repo.getUser()).called(1)
FakeWorking In-Memory ImplementationImplements interface with simplified logicFakeUserRepository using an in-memory List
SpyWraps Real InstanceDelegates calls to real object while recording callsspy(realService)

2. Mocking Frameworks: mocktail vs mockito

1. mocktail (No Code Generation)

Uses Dart features (noSuchMethod) to provide type-safe mocking without running build_runner.

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepo;
  late GetUserUseCase useCase;

  setUp(() {
    mockRepo = MockUserRepository();
    useCase = GetUserUseCase(mockRepo);
  });

  test('returns user when repository call succeeds', () async {
    // 1. Stub response
    when(() => mockRepo.getUser('123'))
        .thenAnswer((_) async => const User(id: '123', name: 'Alice'));

    // 2. Execute
    final result = await useCase.execute('123');

    // 3. Assert & Verify
    expect(result.name, 'Alice');
    verify(() => mockRepo.getUser('123')).called(1);
  });
}

2. mockito (Code Generation Based)

Requires @GenerateMocks([UserRepository]) and running flutter pub run build_runner build.

  • Pros: Strict null-safety guarantees generated at build time.
  • Cons: Slower developer feedback loop due to code generation steps.

3. Mocking Streams & Async Callbacks

When testing reactive BLoCs or Riverpod providers that listen to continuous data streams:

// Mocking a Stream emission sequence
when(() => mockAuthService.authStateStream)
    .thenAnswer((_) => Stream.fromIterable([
          AuthState.unauthenticated(),
          AuthState.authenticated(user),
        ]));

4. Injecting Mocks in Widget Tests

Overriding Riverpod Providers in Widget Tests

testWidgets('Renders user profile when provider yields data', (tester) async {
  final mockUser = User(id: '1', name: 'Alice');

  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        // Override real network provider with mock/fake value!
        userProvider.overrideWith((ref) => Future.value(mockUser)),
      ],
      child: const MaterialApp(home: ProfileScreen()),
    ),
  );

  await tester.pump();
  expect(find.text('Alice'), findsOneWidget);
});

5. Trade-offs & Production Considerations

  • Over-Mocking Fragility: Over-mocking internal implementation details creates fragile tests that break whenever internal code is refactored—even if external behavior remains unchanged. Mock ONLY boundary interfaces (Network, Storage, Platform Channels).
  • Fallback Registration in mocktail: When stubbing methods with custom object parameters in mocktail, register fallback values using registerFallbackValue(MyCustomType()) inside setUpAll().