flutter
/

Mocktail: Mocking Dependencies for BLoC Testing

Last Sync: Today

On this page

14
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Mocktail: Mocking Dependencies for BLoC Testing

Introduction

When testing BLoCs, you often need to isolate the business logic from external dependencies like repositories, API clients, or databases. Mocking allows you to replace real implementations with controlled test doubles. Mocktail is a modern, null‑safe mocking library for Dart that works seamlessly with Flutter and BLoC. This guide covers how to use Mocktail to create mocks, stub methods, verify interactions, and test asynchronous code.

Why Mocktail?

  • Null‑Safe – Fully supports Dart's null safety.
  • No Code Generation – Unlike Mockito, Mocktail doesn't require running build_runner.
  • Simple API – Clean, expressive syntax.
  • Works with Flutter – Compatible with all Flutter and Dart projects.
  • Integration with bloc_test – Seamlessly used inside blocTest.

Setting Up Mocktail

Add Mocktail to your pubspec.yaml dev dependencies:

YAMLRead-only
1
dev_dependencies:
  flutter_test:
    sdk: flutter
  bloc_test: ^9.1.5
  mocktail: ^1.0.0

Creating a Mock

To create a mock, extend Mock and implement the class you want to mock. You can do this inline or as a separate class.

DARTRead-only
1
class MockUserRepository extends Mock implements UserRepository {}

// Or use Mocktail's `mock` function (recommended for one-off mocks)
final mockRepository = mock<UserRepository>();

Stubbing Methods

Use when() to define what a mocked method should return or do when called. Use thenAnswer for futures/streams, thenReturn for synchronous values.

DARTRead-only
1
final mockRepository = MockUserRepository();

// Stub a synchronous method
when(() => mockRepository.getUserId()).thenReturn('123');

// Stub an asynchronous method that returns a Future
when(() => mockRepository.fetchUser('123'))
    .thenAnswer((_) async => User(id: '123', name: 'John'));

// Stub a method that throws
when(() => mockRepository.fetchUser('456'))
    .thenThrow(Exception('User not found'));

// Stub a method that returns a Stream
when(() => mockRepository.userStream())
    .thenAnswer((_) => Stream.value(User(id: '123', name: 'John')));

Verifying Interactions

Use verify() to check that a method was called with specific arguments. You can also verify the number of calls or that it was never called.

DARTRead-only
1
// Verify a method was called once
verify(() => mockRepository.fetchUser('123')).called(1);

// Verify it was never called
verifyNever(() => mockRepository.fetchUser('456'));

// Verify with any argument
verify(() => mockRepository.fetchUser(any())).called(1);

// Verify with argument that matches a predicate
verify(() => mockRepository.fetchUser(argThat(startsWith('1')))).called(1);

Testing BLoC with Mocktail and blocTest

Combine Mocktail with blocTest to test BLoCs that depend on mocked services. The mock is created in the setUp or directly in the build function.

DARTRead-only
1
class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;
  UserBloc(this.repository) : super(UserInitial()) {
    on<LoadUser>(_onLoadUser);
  }

  Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async {
    emit(UserLoading());
    try {
      final user = await repository.fetchUser(event.id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}

// Test
void main() {
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
  });

  group('UserBloc', () {
    blocTest<UserBloc, UserState>(
      'emits [UserLoading, UserLoaded] when user is found',
      build: () => UserBloc(mockRepository),
      setUp: () {
        when(() => mockRepository.fetchUser('123'))
            .thenAnswer((_) async => User(id: '123', name: 'John'));
      },
      act: (bloc) => bloc.add(LoadUser('123')),
      expect: () => [
        UserLoading(),
        UserLoaded(User(id: '123', name: 'John')),
      ],
    );

    blocTest<UserBloc, UserState>(
      'emits [UserLoading, UserError] when repository throws',
      build: () => UserBloc(mockRepository),
      setUp: () {
        when(() => mockRepository.fetchUser('123'))
            .thenThrow(Exception('Not found'));
      },
      act: (bloc) => bloc.add(LoadUser('123')),
      expect: () => [
        UserLoading(),
        UserError('Exception: Not found'),
      ],
    );
  });
}

Working with Async Streams

When your BLoC listens to a stream from a dependency (e.g., a WebSocket or database listener), you can provide a mock stream and control its emissions during the test.

DARTRead-only
1
class ChatBloc extends Bloc<ChatEvent, ChatState> {
  final ChatService chatService;
  late final StreamSubscription _subscription;

  ChatBloc(this.chatService) : super(ChatInitial()) {
    _subscription = chatService.messages.listen((msg) {
      add(MessageReceived(msg));
    });
    on<MessageReceived>((event, emit) {
      emit(ChatLoaded([...state.messages, event.message]));
    });
  }

  @override
  Future<void> close() {
    _subscription.cancel();
    return super.close();
  }
}

// Test
void main() {
  late MockChatService mockChatService;
  late StreamController<Message> streamController;

  setUp(() {
    mockChatService = MockChatService();
    streamController = StreamController<Message>();
    when(() => mockChatService.messages)
        .thenAnswer((_) => streamController.stream);
  });

  blocTest<ChatBloc, ChatState>(
    'emits loaded state with messages when stream emits',
    build: () => ChatBloc(mockChatService),
    act: (bloc) {
      streamController.add(Message(text: 'Hello'));
    },
    expect: () => [
      ChatLoaded([Message(text: 'Hello')]),
    ],
  );

  tearDown(() {
    streamController.close();
  });
}

Capturing Arguments

Sometimes you need to capture the arguments passed to a mocked method to verify them later. Use captureAny or a custom ArgumentMatcher.

DARTRead-only
1
final captured = <String>[];
when(() => mockRepository.saveUser(captureAny())).thenAnswer((invocation) async {
  captured.add(invocation.positionalArguments[0] as String);
  return;
});

// After the act, verify captured arguments
expect(captured, ['expected-value']);

Resetting Mocks

To reset a mock's interactions and stubs between tests, you can create a new instance in setUp. Mocktail doesn't have a built‑in reset method, but recreating the mock is the simplest approach.

DARTRead-only
1
void main() {
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
    // Reset any global state
    reset(mockRepository); // Mocktail provides reset for mocks created with `mock()`
  });
}

Mocking with Generic Types

Mocktail supports generic classes. Use Mock with the generic type or mock<T>().

DARTRead-only
1
class CacheService<T> {
  Future<T?> get(String key);
  Future<void> set(String key, T value);
}

class MockCacheService<T> extends Mock implements CacheService<T> {}

void main() {
  final mockCache = MockCacheService<User>();
  when(() => mockCache.get('user')).thenAnswer((_) async => User(name: 'John'));
}

Best Practices

  • Create mocks in setUp – Ensures a fresh mock for each test.
  • Use any() for arguments you don't care about – Keeps stubs flexible.
  • Verify only essential interactions – Over‑verifying makes tests brittle.
  • Use argThat with predicates – For complex argument matching.
  • Mock only dependencies, not the system under test – Your BLoC should be the real instance.
  • Combine with blocTest – It handles subscriptions and emissions automatically.
  • Keep mocks simple – Avoid stubbing unnecessary methods.

Common Mistakes

  • ❌ Forgetting to stub a method – The mock returns null by default, which may cause Null errors. ✅ Always stub methods that will be called.
  • ❌ Using thenReturn with a Future – Should use thenAnswer for async. ✅ Use thenAnswer for futures and streams.
  • ❌ Not resetting mocks – Stubs from previous tests can leak. ✅ Create new mocks in setUp or use reset().
  • ❌ Mixing Mocktail with Mockito – They use different syntax and can conflict. ✅ Stick to one mocking library.
  • ❌ Stubbing methods with any() but not matching the actual call – Verify that the arguments match your stub.

Conclusion

Mocktail provides a clean, code‑generation‑free way to mock dependencies for BLoC testing. By combining it with blocTest, you can write fast, reliable unit tests that verify your BLoC's behavior in isolation. Mastering Mocktail will significantly improve your testing workflow and code quality.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() => runApp(MyApp());

// Simple BLoC without dependencies
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cubit = context.watch<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Count:', style: TextStyle(fontSize: 20)),
            Text('${cubit.state}', style: TextStyle(fontSize: 40)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: cubit.increment,
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which method is used to define what a mocked method should return?

A
when()
B
stub()
C
mock()
D
then()
Q2
of 3

How do you stub an asynchronous method that returns a Future?

A
thenReturn
B
thenAnswer
C
thenFuture
D
thenAsync
Q3
of 3

What advantage does Mocktail have over Mockito?

A
It's faster
B
No code generation required
C
Better documentation
D
Supports more platforms

Frequently Asked Questions

What is the difference between Mocktail and Mockito?

Mocktail is null‑safe and does not require code generation (no build_runner). Mockito relies on generated code. Mocktail's API is slightly different but generally simpler for most use cases.

Can I use Mocktail with `bloc_test`?

Yes, Mocktail integrates seamlessly with blocTest. You can create mocks in the build function or in setUp and use them inside the test.

How do I mock a method that returns a `Stream`?

Use thenAnswer with a function that returns a Stream. Example: when(() => mock.stream()).thenAnswer((_) => Stream.value('data')).

Can I mock private classes?

Mocktail can only mock public classes because it requires implementing the class. For private classes, consider extracting an interface or making the class public for testing.

How do I verify a method was called with a specific number of times?

Use called(n) after verify. Example: verify(() => mock.method()).called(2).

Previous

bloc unit testing

Next

bloc widget testing

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.