Mocking in Flutter with mocktail: Comparison with mockito

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: flutter

Creating 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 requires when(() => mock.method())
  • mockito needs @GenerateMocks and build_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.

Read more