Testing Flutter Applications

Mocking Dependencies with Mockito and Mocktail

16 min Lesson 4 of 12

Mocking Dependencies with Mockito and Mocktail

A mock is a test double that replaces a real dependency with a controlled substitute. Instead of making real HTTP calls, writing to a real database, or reading from the filesystem, your unit test interacts with a mock that you fully control. This is the foundation of isolated unit testing: the class under test never touches the outside world.

Dart's two dominant mocking libraries are Mockito (code-generation-based, official Google library) and Mocktail (no code generation required, drop-in alternative). Both let you stub return values and verify interactions, but they differ in setup cost and API style.

Note: Mocking is for unit tests only. Integration tests and widget tests that spin up the real widget tree should use real implementations or lightweight fakes, not mocks. Overusing mocks can make tests brittle and hard to maintain.

Why Mocking Matters

Consider a WeatherService that calls a remote API. A unit test for the widget or view-model that depends on it should not make a real network request because:

  • Network calls are slow and flaky — they can fail even if your code is correct.
  • You cannot control what the server returns, making assertions unreliable.
  • Running thousands of tests in CI would hammer an external API.
  • Some code paths (e.g., a 503 error response) are hard to reproduce with a real server.

A mock lets you inject a fake WeatherService that returns exactly the data — or throws exactly the exception — you need for each test scenario.

Mockito: Code-Generation Mocks

Add the dependencies to pubspec.yaml:

dev_dependencies:
  mockito: ^5.4.0
  build_runner: ^2.4.0
  flutter_test:
    sdk: flutter

Annotate your test file with @GenerateMocks, then run dart run build_runner build once to generate the .mocks.dart file. After that, use the generated class in your tests.

// weather_repository.dart
abstract class WeatherRepository {
  Future<String> fetchCondition(String city);
}

// weather_cubit_test.dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';
import 'weather_cubit_test.mocks.dart'; // generated file

@GenerateMocks([WeatherRepository])
void main() {
  late MockWeatherRepository mockRepo;

  setUp(() {
    mockRepo = MockWeatherRepository();
  });

  test('returns condition from repository', () async {
    // Arrange — stub the method
    when(mockRepo.fetchCondition('London'))
        .thenAnswer((_) async => 'Cloudy');

    // Act
    final result = await mockRepo.fetchCondition('London');

    // Assert
    expect(result, equals('Cloudy'));
    verify(mockRepo.fetchCondition('London')).called(1);
  });

  test('throws when repository throws', () async {
    when(mockRepo.fetchCondition(any))
        .thenThrow(Exception('Network error'));

    expect(
      () => mockRepo.fetchCondition('Paris'),
      throwsA(isA<Exception>()),
    );
  });
}
Tip: Run dart run build_runner build --delete-conflicting-outputs every time you add a new class to @GenerateMocks. The generated .mocks.dart file should be committed to version control so CI does not need to run build_runner.

Mocktail: No Code Generation

Mocktail is a popular alternative that removes the build_runner step entirely. You create a mock by extending Mock and implementing the interface — one line per class.

// pubspec.yaml dev_dependencies:
//   mocktail: ^1.0.0

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

// One-line mock declaration — no annotations needed
class MockWeatherRepository extends Mock implements WeatherRepository {}

void main() {
  late MockWeatherRepository mockRepo;

  setUp(() {
    mockRepo = MockWeatherRepository();
    // Register fallback values for types used with any()
    registerFallbackValue('');
  });

  test('returns stubbed condition', () async {
    when(() => mockRepo.fetchCondition('Tokyo'))
        .thenAnswer((_) async => 'Sunny');

    final result = await mockRepo.fetchCondition('Tokyo');

    expect(result, equals('Sunny'));
    verify(() => mockRepo.fetchCondition('Tokyo')).called(1);
  });

  test('verifies method was never called', () async {
    verifyNever(() => mockRepo.fetchCondition(any()));
  });
}
Warning: Mocktail uses arrow-function syntax for when and verifywhen(() => mock.method()) — not the direct call style of Mockito. Mixing the two syntaxes in the same project is a common source of confusion. Stick to one library per project.

Stubbing Return Values

Both libraries support the same fundamental stub operations:

  • thenReturn(value) — returns a plain (non-Future) value synchronously.
  • thenAnswer((_) async => value) — returns a Future or uses the invocation arguments.
  • thenThrow(exception) — throws an exception when the method is called.
  • thenAnswer((_) async => throw exception) — throws asynchronously from a Future.

Verifying Interactions

After the Act phase of a test you can assert not only what was returned but how the mock was called:

  • verify(mock.method(arg)).called(n) — asserts the method was called exactly n times.
  • verifyNever(mock.method()) — asserts the method was never called.
  • verifyInOrder([...]) (Mockito) — asserts calls happened in a specific sequence.
  • verifyNoMoreInteractions(mock) — asserts no further unexpected calls were made.

Summary

Mocking is indispensable for fast, reliable unit tests. Mockito requires a one-time code-generation step but provides strong type safety. Mocktail eliminates that step with a zero-annotation API at the cost of slightly less compile-time safety. Both follow the Arrange–Act–Assert pattern: stub the dependencies in Arrange, exercise the code under test in Act, and assert both return values and mock interactions in Assert.

Key Takeaway: Prefer Mocktail for new projects where fast iteration matters; prefer Mockito when you need strict compile-time guarantees or are working in a large codebase that already uses build_runner. Either way, only mock collaborators you own — avoid mocking third-party classes directly.