Dart 50 🎯 Mocking Dependencies in Unit Tests using mockito and mocktail
A unit test should test one thing. When the unit under the test depends on the database, the network, the clock, or the file system, the test must isolate the unit from those dependencies. Otherwise the test is slow, flaky, and coupled to the external systems. Test doubles are the objects that stand in for the real dependencies. The mock is the test double that records the calls and verifies the interactions. The stub is the test double that returns the canned responses. In the Dart ecosystem, the two libraries for the mocking are the mockito and the mocktail. The mockito uses the code generation for the mocks, and the mocktail uses the manual Mock class with the when and the verify. The two provide the same functionality, and the choice depends on the preference for the code generation or the manual approach.
The mockito is the older and the more established. The @GenerateMocks annotation declares the classes to mock, and the build_runner generates the mock classes in the *.mocks.dart file. The when(mock.method()) stubs the method, and the verify(mock.method()) verifies the call. The advantage is the type safety and the IDE support. The disadvantage is the build step and the generated files .
The mocktail is the newer and the code-generation-free. The mock class extends the Mock and implements the interface, and the when(() => mock.method()) stubs the method. The verify(() => mock.method()) verifies the call. The registerFallbackValue handles the any() for the non-primitive types. The advantage is the no build step and the simpler setup. The disadvantage is the manual boilerplate for the noSuchMethod and the fallback values .
This chapter covers three areas. First, why the mocking exists — the isolation problem and the test-double types. Second, the mockito setup — the @GenerateMocks, the build_runner, the when, the verify, and the matchers. Third, the mocktail setup — the Mock class, the when, the verify, the registerFallbackValue, and the migration between the two. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the mock workflow.
Key point: The mockito generates the mock classes with the @GenerateMocks annotation and the build_runner, and the mocktail uses the manual Mock class without the code generation. The when stubs the method, and the verify checks the call. The mocktail requires the registerFallbackValue for the custom types used with the any() matcher. The two libraries provide the same functionality, and the choice depends on the preference for the code generation or the manual approach.
Why the mocking exists
The isolation problem. The unit under the test depends on the collaborators. The UserService depends on the UserRepository, and the UserRepository depends on the database. Testing the UserService with the real database is the integration test, not the unit test. The database is the slow, the flaky, and the stateful. The test must isolate the UserService from the database, and the mock is the isolation.
The test-double types. The test doubles have the several types. The dummy is the placeholder that is passed but never used. The stub returns the canned answers. The spy records the calls for the later verification. The mock is the pre-programmed object with the expectations. The fake is the simplified implementation that works like the real one. The mockito and the mocktail provide the mock and the stub, and the developer combines them as needed.
The verification problem. The test must verify not only the return value but also the interactions. The UserService.create calls the UserRepository.save with the correct argument. The test verifies the call with the verify(repository.save(user)). The verification catches the bug where the service returns the correct value but does not persist it.
The stubbing problem. The test must control the return value. The UserRepository.find returns the specific user, and the test stubs it with the when(repository.find(1)).thenReturn(user). The stub removes the dependency on the real data, and the test is the deterministic.
The two libraries. The mockito and the mocktail provide the same functionality with the different approaches. The mockito generates the mocks, and the mocktail uses the manual Mock. The mocktail is the choice for the projects without the code generation, and the mockito is the choice for the projects with the build step .
The trade-off. The mocking adds the complexity and the coupling to the test. The test that mocks every dependency is the brittle and the implementation-coupled. The test that uses the real objects for the simple cases and the mocks for the external dependencies is the better. The trade-off is between the isolation and the simplicity, and the discipline is the mocking the external boundaries, not the internal collaborators.
a. The mockito setup
The mockito requires the mockito dev dependency and the build_runner for the code generation.
dev_dependencies:
mockito: ^5.4.0
build_runner: ^2.4.0
The @GenerateMocks annotation declares the classes to mock, and the part directive connects the generated file:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'user_service_test.mocks.dart';
class User {
final int id;
final String name;
User({required this.id, required this.name});
}
abstract class UserRepository {
Future<User?> find(int id);
Future<void> save(User user);
Future<void> delete(int id);
}
@GenerateMocks([UserRepository])
void main() {
late MockUserRepository repository;
late UserService service;
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
});
test('the find returns the user', () async {
final user = User(id: 1, name: 'Alice');
when(repository.find(1)).thenAnswer((_) async => user);
final result = await service.find(1);
expect(result, equals(user));
});
}
The @GenerateMocks([UserRepository]) generates the MockUserRepository in the user_service_test.mocks.dart. The when(repository.find(1)) stubs the method, and the thenAnswer provides the async return. The verify checks the call:
test('the create saves the user', () async {
when(repository.save(any)).thenAnswer((_) async {});
await service.create('Alice');
verify(repository.save(any)).called(1);
});
The any is the matcher that matches any argument, and the .called(1) verifies the call count. The verifyNever verifies the method was not called, and the verifyNoMoreInteractions verifies no other calls .
The build_runner generates the mocks:
dart run build_runner build --delete-conflicting-outputs
The thenReturn and the thenAnswer. The thenReturn provides the synchronous return, and the thenAnswer provides the async:
when(repository.find(1)).thenReturn(user); // the sync
when(repository.find(1)).thenAnswer((_) async => user); // the async
when(repository.find(1)).thenThrow(Exception()); // the throw
The argument matchers. The argThat matches the custom condition, and the captureAny captures the argument:
when(repository.save(argThat(predicate((user) => user.name == 'Alice'))))
.thenAnswer((_) async {});
final captured = verify(repository.save(captureAny)).captured;
expect((captured.first as User).name, equals('Alice'));
b. The mocktail setup
The mocktail requires only the mocktail dev dependency, and there is no code generation:
dev_dependencies:
mocktail: ^1.0.0
The mock class extends the Mock and implements the interface:
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository repository;
late UserService service;
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
});
test('the find returns the user', () async {
final user = User(id: 1, name: 'Alice');
when(() => repository.find(1)).thenAnswer((_) async => user);
final result = await service.find(1);
expect(result, equals(user));
});
}
The MockUserRepository extends Mock implements UserRepository declares the mock. The when(() => repository.find(1)) stubs the method with the closure, and the verify(() => repository.save(any())) verifies the call .
The registerFallbackValue. The mocktail requires the fallback value for the custom types used with the any() matcher:
class User {
final String name;
User({required this.name});
}
void main() {
setUpAll(() {
registerFallbackValue(User(name: ''));
});
test('the create saves the user', () async {
when(() => repository.save(any())).thenAnswer((_) async {});
await service.create('Alice');
verify(() => repository.save(any())).called(1);
});
}
The registerFallbackValue(User(name: '')) registers the placeholder for the User type. Without it, the any() throws the ArgumentError .
The verifyInOrder. The verifyInOrder verifies the call order:
verifyInOrder([
() => repository.save(any()),
() => repository.delete(any()),
]);
The verifyNever and the verifyNoMoreInteractions. The verifyNever verifies the method was not called, and the verifyNoMoreInteractions verifies no other calls:
verifyNever(() => repository.delete(any()));
verifyNoMoreInteractions(repository);
The captureAny and the captureThat. The captureAny captures the argument, and the captureThat captures the matching:
final captured = verify(() => repository.save(captureAny())).captured;
expect((captured.first as User).name, equals('Alice'));
c. The migration and the best practices
The migration from the mockito to the mocktail. The migration replaces the @GenerateMocks with the manual Mock class, the when(mock.method()) with the when(() => mock.method()), the verify(mock.method()) with the verify(() => mock.method()), and the any with the any(). The registerFallbackValue is the new requirement for the custom types .
The mock only the external boundaries. The mocks are for the external dependencies: the database, the network, the file system, the clock. The internal collaborators — the simple value objects, the pure functions — should use the real objects. The over-mocking makes the test the brittle and the implementation-coupled.
The stub the minimum. The test stubs only the methods that the unit calls. The unnecessary stubs are the noise, and the verifyNoMoreInteractions catches the unused. The stub the minimum keeps the test focused.
The verify the behavior, not the implementation. The test verifies the meaningful interactions: the save is called with the user, the delete is called with the id. The test does not verify the internal method calls that are the implementation detail.
The use the setUp for the mocks. The mocks are recreated before each test, and the setUp isolates them:
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
});
The registerFallbackValue in the setUpAll. The fallback values are registered once for the whole test file:
setUpAll(() {
registerFallbackValue(User(name: ''));
registerFallbackValue(Order(id: 0));
});
The named the mock arguments. The any() with the named argument requires the named matcher:
when(() => repository.find(id: any(named: 'id'))).thenAnswer(...);
The thenAnswer for the async. The thenAnswer provides the async return, and the thenReturn is for the sync:
when(() => repository.find(1)).thenAnswer((_) async => user);
Complete Example Session
# ============================================
# PART 1: THE PUBSPEC
# ============================================
dev_dependencies:
test: ^1.24.0
mocktail: ^1.0.0
# or
mockito: ^5.4.0
build_runner: ^2.4.0
# ============================================
# PART 2: THE DOMAIN
# ============================================
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
abstract class UserRepository {
Future<User?> find(int id);
Future<List<User>> findAll();
Future<User> save(User user);
Future<void> delete(int id);
}
class UserService {
final UserRepository _repository;
UserService(this._repository);
Future<User?> find(int id) => _repository.find(id);
Future<List<User>> findAll() => _repository.findAll();
Future<User> create(String name, String email) async {
if (name.isEmpty) {
throw ArgumentError('The name cannot be the empty');
}
if (!email.contains('@')) {
throw ArgumentError('The email is not the valid');
}
final user = User(id: 0, name: name, email: email);
return _repository.save(user);
}
Future<void> delete(int id) => _repository.delete(id);
}
# ============================================
# PART 3: THE MOCKTAIL TEST
# ============================================
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository repository;
late UserService service;
setUpAll(() {
registerFallbackValue(User(id: 0, name: '', email: ''));
});
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
});
group('the find', () {
test('the returns the user', () async {
final user = User(id: 1, name: 'Alice', email: 'alice@example.com');
when(() => repository.find(1)).thenAnswer((_) async => user);
final result = await service.find(1);
expect(result, equals(user));
verify(() => repository.find(1)).called(1);
});
test('the returns the null when the missing', () async {
when(() => repository.find(999)).thenAnswer((_) async => null);
final result = await service.find(999);
expect(result, isNull);
});
});
group('the create', () {
test('the saves the user', () async {
when(() => repository.save(any())).thenAnswer(
(invocation) async => invocation.positionalArguments.first as User,
);
await service.create('Alice', 'alice@example.com');
verify(() => repository.save(any())).called(1);
});
test('the throws when the name is the empty', () async {
expect(
() => service.create('', 'alice@example.com'),
throwsA(isA<ArgumentError>()),
);
verifyNever(() => repository.save(any()));
});
test('the throws when the email is the invalid', () async {
expect(
() => service.create('Alice', 'invalid'),
throwsA(isA<ArgumentError>()),
);
verifyNever(() => repository.save(any()));
});
});
group('the delete', () {
test('the calls the repository', () async {
when(() => repository.delete(1)).thenAnswer((_) async {});
await service.delete(1);
verify(() => repository.delete(1)).called(1);
});
});
}
# ============================================
# PART 4: THE MOCKITO TEST
# ============================================
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'user_service_test.mocks.dart';
@GenerateMocks([UserRepository])
void main() {
late MockUserRepository repository;
late UserService service;
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
});
test('the find returns the user', () async {
final user = User(id: 1, name: 'Alice', email: 'alice@example.com');
when(repository.find(1)).thenAnswer((_) async => user);
final result = await service.find(1);
expect(result, equals(user));
verify(repository.find(1)).called(1);
});
}
# ============================================
# PART 5: THE ARGUMENT MATCHERS
# ============================================
test('the create saves the user with the correct name', () async {
when(() => repository.save(any())).thenAnswer(
(invocation) async => invocation.positionalArguments.first as User,
);
await service.create('Alice', 'alice@example.com');
final captured = verify(() => repository.save(captureAny())).captured;
final savedUser = captured.first as User;
expect(savedUser.name, equals('Alice'));
expect(savedUser.email, equals('alice@example.com'));
});
# ============================================
# PART 6: THE THENANSWer
# ============================================
test('the find returns the user', () async {
when(() => repository.find(1)).thenAnswer((_) async {
await Future.delayed(Duration(milliseconds: 10));
return User(id: 1, name: 'Alice', email: 'alice@example.com');
});
final result = await service.find(1);
expect(result?.name, equals('Alice'));
});
# ============================================
# PART 7: THE THROWING
# ============================================
test('the service propagates the error', () async {
when(() => repository.find(1)).thenThrow(Exception('Database error'));
expect(
() => service.find(1),
throwsA(isA<Exception>()),
);
});
# ============================================
# PART 8: THE VERIFY IN ORDER
# ============================================
test('the create then delete', () async {
when(() => repository.save(any())).thenAnswer(
(_) async => User(id: 1, name: 'Alice', email: 'alice@example.com'),
);
when(() => repository.delete(any())).thenAnswer((_) async {});
final user = await service.create('Alice', 'alice@example.com');
await service.delete(user.id);
verifyInOrder([
() => repository.save(any()),
() => repository.delete(user.id),
]);
});
# ============================================
# PART 9: THE VERIFY NEVER
# ============================================
test('the create does not save the invalid', () async {
expect(
() => service.create('', 'invalid'),
throwsA(isA<ArgumentError>()),
);
verifyNever(() => repository.save(any()));
verifyNoMoreInteractions(repository);
});
# ============================================
# PART 10: THE COMPLETE EXAMPLE
# ============================================
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
abstract class UserRepository {
Future<User?> find(int id);
Future<User> save(User user);
Future<void> delete(int id);
}
class UserService {
final UserRepository _repository;
UserService(this._repository);
Future<User?> find(int id) => _repository.find(id);
Future<User> create(String name, String email) async {
if (name.isEmpty) throw ArgumentError('The name is the required');
if (!email.contains('@')) throw ArgumentError('The email is the invalid');
return _repository.save(User(id: 0, name: name, email: email));
}
Future<void> delete(int id) => _repository.delete(id);
}
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository repository;
late UserService service;
setUpAll(() {
registerFallbackValue(User(id: 0, name: '', email: ''));
});
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
});
group('the UserService', () {
group('the find', () {
test('the returns the user when the found', () async {
final user = User(id: 1, name: 'Alice', email: 'alice@example.com');
when(() => repository.find(1)).thenAnswer((_) async => user);
final result = await service.find(1);
expect(result, equals(user));
verify(() => repository.find(1)).called(1);
});
test('the returns the null when the missing', () async {
when(() => repository.find(999)).thenAnswer((_) async => null);
expect(await service.find(999), isNull);
});
});
group('the create', () {
test('the saves the valid user', () async {
when(() => repository.save(any())).thenAnswer(
(invocation) async => invocation.positionalArguments.first as User,
);
await service.create('Alice', 'alice@example.com');
final captured = verify(() => repository.save(captureAny())).captured;
final saved = captured.first as User;
expect(saved.name, equals('Alice'));
expect(saved.email, equals('alice@example.com'));
});
test('the throws when the name is the empty', () async {
expect(
() => service.create('', 'alice@example.com'),
throwsA(isA<ArgumentError>()),
);
verifyNever(() => repository.save(any()));
});
test('the throws when the email is the invalid', () async {
expect(
() => service.create('Alice', 'invalid'),
throwsA(isA<ArgumentError>()),
);
verifyNever(() => repository.save(any()));
});
});
group('the delete', () {
test('the calls the repository', () async {
when(() => repository.delete(1)).thenAnswer((_) async {});
await service.delete(1);
verify(() => repository.delete(1)).called(1);
});
test('the propagates the repository error', () async {
when(() => repository.delete(1)).thenThrow(Exception('Not found'));
expect(
() => service.delete(1),
throwsA(isA<Exception>()),
);
});
});
});
}
The ten parts show the pubspec, the domain, the mocktail test, the mockito test, the argument matchers, the thenAnswer, the throwing, the verifyInOrder, the verifyNever, and the complete example.
Quick Reference
The Two Libraries
| Aspect | mockito | mocktail |
|---|---|---|
| The code generation | Yes | No |
| The annotation | @GenerateMocks | The manual Mock |
| The build step | build_runner | None |
The when | when(mock.method()) | when(() => mock.method()) |
The verify | verify(mock.method()) | verify(() => mock.method()) |
The any | any | any() |
| The fallback | Not required | registerFallbackValue |
The Stubbing
| The method | The purpose |
|---|---|
when(...).thenReturn(v) | The sync return |
when(...).thenAnswer((_) async => v) | The async return |
when(...).thenThrow(e) | The throw |
when(...).thenAnswer((inv) => ...) | The access to the arguments |
The Verification
| The method | The purpose |
|---|---|
verify(...).called(n) | The call count |
verifyNever(...) | The never called |
verifyInOrder([...]) | The call order |
verifyNoMoreInteractions(mock) | The no other calls |
The Matchers
| The matcher | The purpose |
|---|---|
any() | The any argument |
any(named: 'name') | The named argument |
captureAny() | The capture |
captureThat(matcher) | The capture with the matcher |
argThat(predicate) | The custom matcher |
The Custom Types
| The library | The approach |
|---|---|
mocktail | The registerFallbackValue in the setUpAll |
mockito | The generated mock and the any |
Best Practices
✅ Do This:
// Use the setUp to create the fresh mocks
setUp(() {
repository = MockUserRepository();
service = UserService(repository);
}); // ✅
// Use the registerFallbackValue for the custom types
setUpAll(() {
registerFallbackValue(User(id: 0, name: '', email: ''));
}); // ✅
// Use the when to stub the method
when(() => repository.find(1)).thenAnswer((_) async => user); // ✅
// Use the verify to check the call
verify(() => repository.save(any())).called(1); // ✅
// Use the captureAny to capture the arguments
final captured = verify(() => repository.save(captureAny())).captured; // ✅
// Mock only the external boundaries
// The mock for the repository, not the pure functions // ✅
// Use the verifyNever for the error cases
verifyNever(() => repository.save(any())); // ✅
❌ Don’t Do This:
// Don't forget the registerFallbackValue
when(() => repository.save(any())).thenAnswer(...); // the ArgumentError // ❌
// Don't mock the pure functions
final mock = MockMath(); // the unnecessary // ❌
// Don't stub the unused methods
when(() => repository.delete(any())).thenAnswer(...); // the unused // ❌
// Don't verify the implementation details
verify(() => repository._internalCall()).called(1); // the private // ❌
// Don't share the mock between the tests
final repository = MockUserRepository(); // the shared state // ❌
// Don't use the thenReturn for the async
when(() => repository.find(1)).thenReturn(user); // the missing Future // ❌
// Don't forget the await in the async test
test('the async', () { service.create('A', 'a@b.com'); }); // the missing await // ❌
Common Pitfalls
| The pitfall | The why | The fix |
|---|---|---|
The ArgumentError on the any() | The missing fallback | Use the registerFallbackValue |
The type 'Null' is not the subtype | The thenReturn for the async | Use the thenAnswer |
| The test passes but the code is wrong | The missing verify | Add the verify |
| The brittle test | The over-mocking | Mock only the boundaries |
| The shared mock state | The mock in the main | Use the setUp |
The verify fails | The wrong argument | Check the matcher |
| The async not awaited | The missing await | Add the await |
Real-World Examples
1. The Basic Mock
class MockUserRepository extends Mock implements UserRepository {}
2. The Stub
when(() => repository.find(1)).thenAnswer((_) async => user);
3. The Verify
verify(() => repository.save(any())).called(1);
4. The Capture
final captured = verify(() => repository.save(captureAny())).captured;
5. The Fallback
registerFallbackValue(User(id: 0, name: '', email: ''));
6. The Throw
when(() => repository.find(1)).thenThrow(Exception('Error'));
7. The Never
verifyNever(() => repository.save(any()));
8. The In Order
verifyInOrder([() => repository.save(any()), () => repository.delete(1)]);
9. The Named
when(() => repository.find(id: any(named: 'id'))).thenAnswer(...);
10. The Complete
setUp(() { repository = MockUserRepository(); service = UserService(repository); });
Visual
The Mock Workflow
┌──────────────────────────────────────────────────────────────┐
│ THE MOCK WORKFLOW │
│ │
│ 1. The mock class: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ class MockUserRepository extends Mock │ │
│ │ implements UserRepository {} │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. The stub: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ when(() => repository.find(1)) │ │
│ │ .thenAnswer((_) async => user); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. The act: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ final result = await service.find(1); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. The verify: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ expect(result, equals(user)); │ │
│ │ verify(() => repository.find(1)).called(1); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Stub and the Verify
┌──────────────────────────────────────────────────────────────┐
│ THE STUB AND THE VERIFY │
│ │
│ THE STUB (the input): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ when(() => repository.find(1)) │ │
│ │ .thenAnswer((_) async => user); │ │
│ │ │ │
│ │ "When the find is called with the 1, return the user"│ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE VERIFY (the output): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ verify(() => repository.save(any())).called(1); │ │
│ │ │ │
│ │ "Verify the save was called exactly once" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The stub controls the inputs; the verify checks the outputs.│
│ │
└──────────────────────────────────────────────────────────────┘
The registerFallbackValue
┌──────────────────────────────────────────────────────────────┐
│ THE registerFallbackValue │
│ │
│ class User { │
│ final int id; │
│ final String name; │
│ User({required this.id, required this.name}); │
│ } │
│ │
│ setUpAll(() { │
│ registerFallbackValue(User(id: 0, name: '')); │
│ }); │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The any() matcher needs the placeholder for the │ │
│ │ custom type. │ │
│ │ │ │
│ │ Without the registerFallbackValue: │ │
│ │ when(() => repository.save(any())) │ │
│ │ → the ArgumentError: the fallback not registered │ │
│ │ │ │
│ │ With the registerFallbackValue: │ │
│ │ The any() works. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Mockito vs the Mocktail
┌──────────────────────────────────────────────────────────────┐
│ THE MOCKITO vs THE MOCKTAIL │
│ │
│ THE MOCKITO: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @GenerateMocks([UserRepository]) │ │
│ │ void main() { │ │
│ │ final repository = MockUserRepository(); │ │
│ │ │ │
│ │ when(repository.find(1)) │ │
│ │ .thenAnswer((_) async => user); │ │
│ │ │ │
│ │ verify(repository.save(any)).called(1); │ │
│ │ } │ │
│ │ │ │
│ │ The build_runner generates the MockUserRepository. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE MOCKTAIL: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ class MockUserRepository extends Mock │ │
│ │ implements UserRepository {} │ │
│ │ │ │
│ │ void main() { │ │
│ │ final repository = MockUserRepository(); │ │
│ │ │ │
│ │ when(() => repository.find(1)) │ │
│ │ .thenAnswer((_) async => user); │ │
│ │ │ │
│ │ verify(() => repository.save(any())).called(1); │ │
│ │ } │ │
│ │ │ │
│ │ No code generation. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
mockito | The code-generation mocking library |
mocktail | The manual mocking library |
Mock | The base class for the manual mocks |
when | The stubbing |
thenReturn | The sync return |
thenAnswer | The async return |
thenThrow | The throw |
verify | The call verification |
verifyNever | The not-called |
verifyInOrder | The call order |
captureAny | The argument capture |
registerFallbackValue | The custom type fallback |
Key takeaways:
- The mocking isolates the unit from the external dependencies. The database, the network, the file system, and the clock are the external boundaries, and the mock stands in for them. The unit test tests the unit, not the integration .
- The
mockitogenerates the mocks with the@GenerateMocksand thebuild_runner. The advantage is the type safety and the IDE support. The disadvantage is the build step and the generated files . - The
mocktailuses the manualMockclass without the code generation. The mock class extends theMockand implements the interface. The advantage is the no build step. The disadvantage is the manualregisterFallbackValuefor the custom types . - The
whenstubs the method, and theverifychecks the call. ThethenReturnprovides the sync return, thethenAnswerthe async, and thethenThrowthe exception. Theverifychecks the call count, and theverifyNeverthe not-called . - The
registerFallbackValueis required for the custom types. Theany()matcher needs the placeholder for the non-primitive types. Without it, theany()throws theArgumentError. ThesetUpAllis the place for the registration . - The
captureAnycaptures the arguments. Theverify(() => repository.save(captureAny())).capturedreturns the list of the captured arguments, and the test asserts on the captured values . - The mock only the external boundaries. The over-mocking makes the test the brittle and the implementation-coupled. The internal collaborators — the pure functions, the value objects — should use the real objects .
- The
setUpcreates the fresh mocks. The mocks are recreated before each test, and the state is isolated. The shared mock state is the source of the flaky tests .
Remember: The mocking isolates the unit from the external dependencies. The mockito and the mocktail are the two libraries, and the mocktail is the code-generation-free. The when stubs the method, the verify checks the call, and the registerFallbackValue handles the custom types. The captureAny captures the arguments, and the verifyNever checks the not-called. The result is the isolated, the fast, and the reliable unit tests, which is the foundation of the testable code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!