Mocking in Flutter with mocktail: Comparison with mockito
Mocking in Flutter means replacing real dependencies (API clients, database repositories, platform services) with test doubles that you control. Mocktail is the modern choice: it doesn't require code generation, works with null safety out of the box, and has a clean API. Mockito is the alternative — it requires build_runner code generation but is more mature. This guide covers mocktail with a comparison to mockito.
Key Takeaways
Mocktail needs no code generation. Just extend Mock and implement the interface. No @GenerateMocks annotation, no build_runner, no generated files to commit.
when(() => mock.method()).thenReturn(value) is the core pattern. Use arrow function syntax for the stub call — this is mocktail's way of capturing the call without executing it.
any() is the catch-all argument matcher. when(() => mock.fetch(any())).thenReturn(...) matches any argument. For specific values, use the exact value directly.
verify(() => mock.method()).called(1) asserts call count. Use verifyNever(() => mock.method()) to assert it was never called. Call verifyNoMoreInteractions(mock) to assert no unexpected calls.
thenThrow() tests error handling paths. when(() => mock.fetch(any())).thenThrow(Exception('Network error')) triggers your error handling code.
Why Mock in Flutter Tests?
Flutter widgets and services depend on each other. A UserProfileWidget depends on UserRepository, which depends on ApiClient, which makes real HTTP requests.
In tests, you don't want real HTTP requests:
- They're slow
- They're flaky (network failures, test data changes)
- They require a real server
Mocking replaces UserRepository with a MockUserRepository that you control — returning predictable data without network calls.
mocktail vs mockito
| Feature | mocktail | mockito |
|---|---|---|
| Code generation | No | Yes (build_runner) |
| Null safety | Full | Full (v5+) |
| API style | when(() => ...) |
when(mock.method()) |
| Argument matchers | any(), captureAny() |
any, argThat() |
| Maturity | Newer | Older, more battle-tested |
| Community | Growing | Large |
Choose mocktail if you want simpler setup and no generated code.
Choose mockito if you need mature tooling, are on a large existing codebase, or need advanced argument matchers.
Setting Up mocktail
# pubspec.yaml
dev_dependencies:
mocktail: ^1.0.4
flutter_test:
sdk: flutterCreating Mocks
Define an interface/abstract class for your dependency:
// lib/repositories/user_repository.dart
abstract class UserRepository {
Future<User> getUserById(String id);
Future<List<User>> getAllUsers();
Future<void> createUser(User user);
Future<void> deleteUser(String id);
Stream<List<User>> watchUsers();
}Create a mock:
// test/mocks/mock_user_repository.dart
import 'package:mocktail/mocktail.dart';
import 'package:my_app/repositories/user_repository.dart';
class MockUserRepository extends Mock implements UserRepository {}That's it. No code generation needed.
Using Mocks in Tests
// test/widgets/user_list_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/widgets/user_list.dart';
import 'package:my_app/repositories/user_repository.dart';
import 'mocks/mock_user_repository.dart';
void main() {
late MockUserRepository mockRepo;
setUp(() {
mockRepo = MockUserRepository();
});
group('UserList', () {
testWidgets('displays users from repository', (tester) async {
// Setup
when(() => mockRepo.getAllUsers()).thenAnswer(
(_) async => [
User(id: '1', name: 'Alice', email: 'alice@example.com'),
User(id: '2', name: 'Bob', email: 'bob@example.com'),
],
);
// Build
await tester.pumpWidget(MaterialApp(
home: UserList(repository: mockRepo),
));
await tester.pumpAndSettle();
// Assert
expect(find.text('Alice'), findsOneWidget);
expect(find.text('Bob'), findsOneWidget);
verify(() => mockRepo.getAllUsers()).called(1);
});
testWidgets('shows error message on repository failure', (tester) async {
when(() => mockRepo.getAllUsers()).thenThrow(
Exception('Failed to load users'),
);
await tester.pumpWidget(MaterialApp(
home: UserList(repository: mockRepo),
));
await tester.pumpAndSettle();
expect(find.text('Failed to load users'), findsOneWidget);
});
});
}Stub Patterns
Return a value
when(() => mockRepo.getUserById('1')).thenReturn(
User(id: '1', name: 'Alice'),
);Return a Future
when(() => mockRepo.getUserById('1')).thenAnswer(
(_) async => User(id: '1', name: 'Alice'),
);Throw an exception
when(() => mockRepo.getUserById(any())).thenThrow(
NotFoundException('User not found'),
);Return a Stream
when(() => mockRepo.watchUsers()).thenAnswer(
(_) => Stream.fromIterable([
[User(id: '1', name: 'Alice')],
[User(id: '1', name: 'Alice'), User(id: '2', name: 'Bob')],
]),
);Different answers per call
var callCount = 0;
when(() => mockRepo.getAllUsers()).thenAnswer((_) async {
callCount++;
if (callCount == 1) throw Exception('First call fails');
return [User(id: '1', name: 'Alice')];
});Argument Matchers
// Match any argument
when(() => mockRepo.getUserById(any())).thenAnswer(
(_) async => User(id: '1', name: 'Alice'),
);
// Match specific value
when(() => mockRepo.getUserById('admin')).thenAnswer(
(_) async => User(id: 'admin', name: 'Admin', role: Role.admin),
);
// Custom matcher
when(() => mockRepo.getUserById(any(that: startsWith('user_')))).thenAnswer(
(_) async => User(id: '1', name: 'Regular User'),
);Verification
// Verify called exactly N times
verify(() => mockRepo.getAllUsers()).called(1);
verify(() => mockRepo.deleteUser('1')).called(1);
// Verify never called
verifyNever(() => mockRepo.createUser(any()));
// Verify with specific arguments
verify(() => mockRepo.getUserById('1')).called(1);
// Verify order
verifyInOrder([
() => mockRepo.getAllUsers(),
() => mockRepo.deleteUser('1'),
() => mockRepo.getAllUsers(),
]);
// Verify no unexpected calls
verifyNoMoreInteractions(mockRepo);Argument Capture
Capture the actual arguments passed to a mock for more detailed assertions:
testWidgets('creates user with correct data', (tester) async {
when(() => mockRepo.createUser(any())).thenAnswer((_) async {});
await tester.pumpWidget(MaterialApp(
home: CreateUserForm(repository: mockRepo),
));
await tester.enterText(find.byKey(const Key('name-field')), 'Alice');
await tester.enterText(find.byKey(const Key('email-field')), 'alice@example.com');
await tester.tap(find.byKey(const Key('submit-btn')));
await tester.pumpAndSettle();
final captured = verify(() => mockRepo.createUser(captureAny())).captured;
final createdUser = captured.first as User;
expect(createdUser.name, equals('Alice'));
expect(createdUser.email, equals('alice@example.com'));
});Registering Fallback Values
Mocktail requires fallback values for custom types used with any():
// In your test setup (in setUpAll or at the top of the test file)
setUpAll(() {
registerFallbackValue(User(id: '', name: '', email: ''));
registerFallbackValue(const Duration(seconds: 0));
});This is only needed for custom types. Built-in types (String, int, bool, etc.) work without registration.
Comparison with mockito
The same test with mockito:
// Using mockito (requires code generation)
@GenerateMocks([UserRepository])
void main() {
// ...
// Setup
when(mockRepo.getAllUsers()).thenAnswer(
(_) async => [User(id: '1', name: 'Alice')],
);
// Verify
verify(mockRepo.getAllUsers()).called(1);
}The main differences:
- mockito uses
when(mock.method())directly; mocktail requireswhen(() => mock.method()) - mockito needs
@GenerateMocksandbuild_runner; mocktail doesn't - mockito's verification uses direct calls; mocktail uses lambdas
Both approaches work. Mocktail's lambda style is more explicit about what's being stubbed vs executed.
Testing Service Logic with Mocks
Testing a service that depends on a repository:
// lib/services/user_service.dart
class UserService {
final UserRepository _repository;
UserService(this._repository);
Future<User?> findUserByEmail(String email) async {
final users = await _repository.getAllUsers();
return users.where((u) => u.email == email).firstOrNull;
}
}// test/services/user_service_test.dart
void main() {
late MockUserRepository mockRepo;
late UserService service;
setUp(() {
mockRepo = MockUserRepository();
service = UserService(mockRepo);
});
test('finds user by email', () async {
when(() => mockRepo.getAllUsers()).thenAnswer((_) async => [
User(id: '1', name: 'Alice', email: 'alice@example.com'),
User(id: '2', name: 'Bob', email: 'bob@example.com'),
]);
final user = await service.findUserByEmail('alice@example.com');
expect(user?.name, equals('Alice'));
});
test('returns null when user not found', () async {
when(() => mockRepo.getAllUsers()).thenAnswer((_) async => []);
final user = await service.findUserByEmail('notfound@example.com');
expect(user, isNull);
});
}Production Services
Mocks replace real dependencies in tests. Your production code uses the real UserRepository which calls real APIs.
HelpMeTest monitors those real APIs continuously — checking that the services your Flutter app depends on are working 24/7. Start free with 10 monitored tests.