In a Flutter widget test, how do you replace a real Bloc with bloc_test's MockBloc and drive its states with whenListen?
answer
- type arguments on MockBloc
- stream empty, close stubbed
- state getter unstubbed
- whenListen keeps state in sync
- BlocProvider.value injects the mock
basics
~10 sDeclare MockCartBloc extends MockBloc<CartEvent, CartState> implements CartBloc, provide it with BlocProvider.value, stub its state, and use whenListen to feed a stream of states while keeping the state getter in sync.
solid answer
~40 sbloc_test's `MockBloc<E, S>` (and `MockCubit<S>`) extends mocktail's `Mock`; its constructor stubs `stream` to an empty stream and `close` to a completed future, but not `state`, so an unstubbed `state` returns null and fails as a type error for a non-nullable state. Declare `class MockCartBloc extends MockBloc<CartEvent, CartState> implements CartBloc {}` with explicit type arguments, stub the current state with `when(() => bloc.state).thenReturn(...)`, and hand the mock to the widget with `BlocProvider<CartBloc>.value`. `whenListen(bloc, Stream.fromIterable([...]), initialState: ...)` stubs `stream` with a broadcast stream and updates the `state` stub as each state is delivered, so builders and listeners see a consistent sequence. Assert the UI, and verify events the widget added to the mock.
code
dart · 36 linesimport 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shop/cart/cart.dart';
class MockCartBloc extends MockBloc<CartEvent, CartState>
implements CartBloc {}
void main() {
const apple = CartItem(id: 'apple', price: 120);
late MockCartBloc cartBloc;
setUp(() => cartBloc = MockCartBloc());
testWidgets('shows the empty message once the cart is cleared', (tester) async {
whenListen(
cartBloc,
Stream.fromIterable(const [CartState(items: [])]),
initialState: const CartState(items: [apple]),
);
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<CartBloc>.value(
value: cartBloc,
child: const CartView(),
),
),
);
await tester.pump();
expect(find.text('Your cart is empty'), findsOneWidget);
});
}go deeper
Recall that bloc_test provides MockBloc and MockCubit, and that the mock is handed to the widget with BlocProvider.value.
Explain what MockBloc stubs by default, why state must be stubbed, and how whenListen keeps state and stream in sync.
Structure screens so the bloc is injected, test listeners through streamed states, and verify events rather than internals.
Split coverage deliberately: blocTest for business rules, mocked-bloc widget tests for rendering, and a few integration tests with real blocs.
## Why mock the bloc in a widget test A widget test for a cart screen should prove that the screen renders each `CartState` correctly and adds the right events. A real `CartBloc` drags in repositories, timers and network stubs, and reaching a rare state (a failed checkout) through real events is slow. bloc_test ships **`MockBloc<E, S>`** and **`MockCubit<S>`** so the widget can be driven by canned states instead. ## What MockBloc gives you From the bloc_test 10 source, both classes extend mocktail's `Mock` and implement the bloc interface. The constructor adds two stubs: - `stream` returns an empty stream; - `close` returns an already-completed future. Everything else, including the **`state` getter**, is unstubbed. An unstubbed call on a mocktail mock returns `null` by default, so reading `state` of a non-nullable type fails with a type error the moment a `BlocBuilder` builds. Stubbing the state is therefore the first line of almost every such test. ### Type arguments are not optional Declare the mock as: ```dart class MockCartBloc extends MockBloc<CartEvent, CartState> implements CartBloc {} ``` Without the type arguments the class would implement `Bloc<dynamic, dynamic>` through `MockBloc` and `Bloc<CartEvent, CartState>` through `CartBloc` - two different type arguments for the same generic interface, which Dart rejects. bloc_test's own documentation marks the bare form as wrong. ## whenListen `whenListen(bloc, stream, {initialState})` is the helper for state sequences: 1. It converts your stream to a broadcast stream, so several widgets can listen. 2. It stubs `bloc.stream` to return that stream. 3. As each state passes through, it re-stubs `bloc.state` to that value, so `state` and `stream` stay consistent. 4. If `initialState` is given, it stubs `state` to that value before anything is delivered. This matters because `BlocBuilder` reads `bloc.state` for its first build and then listens to `bloc.stream`, while `BlocListener` reacts only to states arriving on the stream. A test for a SnackBar shown on a failed checkout therefore needs the failure to arrive through `whenListen`, not just as the stubbed initial state. ## Injecting the mock Wrap the widget under test in `BlocProvider<CartBloc>.value(value: cartBloc, child: const CartView())`. The `.value` form hands over an existing instance without taking ownership, which is what you want for a mock the test owns. The screen should read the bloc from context rather than construct it - a widget that creates its own `CartBloc` cannot be given a mock. ## Asserting - **Rendering** - `find.text`, `find.byType` against each state. - **Events** - tap a button, then `verify(() => cartBloc.add(const CartCleared())).called(1)`. Value-equal events make the argument match. - **Timing** - `Stream.fromIterable` delivers asynchronously, so pump after `pumpWidget` before asserting on streamed states. | Need | Tool | |---|---| | one fixed state | `when(() => bloc.state).thenReturn(...)` | | a sequence of states | `whenListen(bloc, stream)` | | a starting state plus a sequence | `whenListen(..., initialState: ...)` | | check an added event | mocktail `verify` on `bloc.add` | ## Pitfalls - Forgetting to stub `state` - the null type error. - A bare `MockBloc` without type arguments - a compile error. - Expecting a `BlocListener` to fire for the stubbed initial state - it only reacts to streamed states. - Asserting streamed states without pumping.
- Why does a BlocListener-driven SnackBar not appear when you only stub cartBloc.state?`BlocListener` subscribes to `bloc.stream` and reacts to states that arrive on it; the current `state` is never delivered as a change. With only `state` stubbed, `MockBloc`'s default stream is empty, so the listener never fires. Deliver the failure state through `whenListen` and pump.
- How do you check that tapping Clear adds CartCleared to the mock?Tap the button, then use mocktail's `verify(() => cartBloc.add(const CartCleared())).called(1)`. `add` on the mock is recorded like any other call; value-equal events let the expected argument match the one the widget created.
MockBloc is an understudy who knows no lines: ask it for the current state before giving it a script and it has nothing to say. whenListen hands it the script and updates its current line as each one is spoken.
saying these in an interview costs you the question
- Declares MockCartBloc extends MockBloc implements CartBloc without type arguments.
- Expects MockBloc to return the real bloc's initial state by default.
- Stubs only state and expects BlocListener callbacks to fire.
- Lets the screen construct its own CartBloc, then cannot inject the mock.
- Asserts on streamed states right after pumpWidget without pumping again.