skip to content

Unit Test Helpers

bloc_test's blocTest drives a bloc through build, seed, act and wait, then asserts the exact states emitted. Interviewers ask how you test event transformers and widgets that depend on a bloc.

on this pageshow

explore

questions

5

With the bloc_test package, how does blocTest drive a Bloc through build, act and expect, and why is the initial state missing from expect?

level: juniorimportance: must knowfreq 50%

answer

  1. fresh bloc per test
  2. subscribe, act, then close
  3. exact ordered list of states
  4. stream never carries the constructor state
  5. verify runs after expect

basics

~20 s

blocTest builds a fresh bloc, subscribes to its state stream, runs act, closes the bloc and compares every state emitted after that against expect, in order. The constructor's initial state is never pushed to the stream, so it is never recorded.

solid answer

~30 s

`blocTest<CartBloc, CartState>` from bloc_test registers an ordinary test. `build` returns a new bloc, blocTest subscribes to `bloc.stream`, awaits `act` (for example `bloc.add(CartItemAdded(apple))`), lets pending work run, calls `close()` and only then matches the recorded states against whatever `expect` returns. The match is an exact ordered list, so a missing or an extra state fails. The initial state passed to `super(...)` is only in `bloc.state`; it was never emitted on the stream, so it does not belong in `expect`. After the match, `verify` receives the bloc for interaction checks on mocked collaborators, and `errors` asserts on errors the bloc reported.

code

dart · 24 lines
dart
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';

import 'package:shop/cart/cart_bloc.dart';

class MockCartRepository extends Mock implements CartRepository {}

void main() {
  const apple = CartItem(id: 'apple', price: 120);
  late MockCartRepository repository;

  setUp(() {
    repository = MockCartRepository();
    when(() => repository.save(any())).thenAnswer((_) async {});
  });

  blocTest<CartBloc, CartState>(
    'emits a cart holding the apple when CartItemAdded is added',
    build: () => CartBloc(repository: repository),
    act: (bloc) => bloc.add(const CartItemAdded(apple)),
    expect: () => const [CartState(items: [apple])],
    verify: (_) => verify(() => repository.save(any())).called(1),
  );
}

go deeper

for a junior

Recall the three core parameters: build makes the bloc, act drives it, expect lists the states it should emit. Remember the initial state is not in that list.

for a middle

Explain the run order from the source: subscribe, act, close, compare. Be ready to say why closing first makes an extra state fail the test.

for a senior

Show how verify and errors fit the flow, and how a thrown handler exception fails a test that did not declare errors. Tie test design to one behaviour per case.

for a principal

Discuss what a team standard for bloc tests should require: fresh instances, value-equal states, explicit errors, and where widget tests take over from bloc tests.

## What blocTest is `blocTest` is the main helper in the **bloc_test** package (version 10 at the time of writing, paired with bloc 9). It wraps a single test case around one bloc or cubit and does the plumbing for you: creating the instance, listening to it, driving it, closing it and comparing what it emitted with what you expected. Since bloc_test 10 its type bound is `EmittableStateStreamableSource<State>` rather than `BlocBase`, but in practice you pass a `Bloc` or a `Cubit`: - `build` - required; returns the instance under test. It runs once per test, so every case starts clean. - `act` - optional; receives the instance and interacts with it (`bloc.add(...)` for a Bloc, a method call for a Cubit). blocTest awaits whatever it returns. - `expect` - optional; a function returning the expected states, either a `List` of concrete states or any `Matcher`. - `verify` - optional; called with the instance after the state comparison. - `errors` - optional; a function returning a matcher for the errors the instance reported. - `seed`, `skip`, `wait`, `setUp`, `tearDown` and `tags` - the less frequent knobs. ## The order it runs in Reading the package source, one blocTest case runs these steps: 1. Run `setUp`, then call `build`. 2. If `seed` is given, emit the seeded state directly on the instance. 3. Subscribe to `bloc.stream`, dropping the first `skip` states (default `0`), and collect the rest into a list. 4. Await `act`. 5. If `wait` is given, sleep for that `Duration`; then yield once more so queued work can finish. 6. Call `close()` on the instance. 7. Compare the collected list with `expect()`; run `verify`; run `tearDown`. 8. If `errors` is given, compare the reported errors with `errors()`. Closing before comparing is the point of step 6: once the instance is closed nothing else can be emitted, so the list you compare is complete and an extra, unexpected state makes the test fail. ## Why the initial state is not in the list A bloc's state stream is a **broadcast stream** that only carries states produced by `emit`. The value passed to the constructor (`super(const CartState())`) becomes `state` directly; nothing is added to the stream for it, and a new subscriber is not replayed the current value. So for a `CartBloc` starting empty, adding one `CartItemAdded` produces exactly one recorded state - the cart with that item. If you want to assert the starting value, assert `CartBloc().state` in a plain test. ## Matching: equality or matchers A `List` returned from `expect` is compared element by element with `==`. That works when the state classes implement value equality (usually by extending `Equatable`). When they do not, return matchers instead, such as `isA<CartState>()`, or use `having` to check one field. ## verify and errors - `verify` is the place for interaction checks on collaborators, for example that `CartRepository.save` was called once. It runs after `expect`, with the same instance `build` returned. - `errors` records errors the bloc reported through `onError` - an exception thrown inside an event handler, or a call to `addError`. If a handler throws and you did **not** pass `errors`, the exception propagates and the test fails even when the states matched. Errors reported through `addError` alone do not fail the test; they are only asserted when you pass `errors`. ## Checklist - One `blocTest` per behaviour, with a fresh instance from `build`. - Never list the initial state in `expect`. - `add` returns `void`; there is nothing to await for the handler itself - blocTest lets queued work run before closing. - Put mocked-collaborator checks in `verify`, error assertions in `errors`.

  • A CartBloc handler throws CartSyncException and the blocTest has no errors argument. What happens?
    The test fails with that exception, even if the recorded states match `expect`. blocTest runs the case in a guarded zone and rethrows errors it was not told to expect. Pass `errors: () => [isA<CartSyncException>()]` to record and assert it instead. An error reported only through `addError` is recorded but does not fail the test on its own.
  • Why not share one CartBloc instance across several blocTest cases?
    blocTest closes the instance at the end of every case. A shared instance is closed after the first test, and adding an event to a closed Bloc throws a StateError. `build` exists so each case gets a fresh instance with a known starting state.

saying these in an interview costs you the question

  • Lists the initial state first in expect, as if the stream replayed it.
  • Believes blocTest passes as long as the expected states appear somewhere in the output.
  • Tries to await bloc.add(), thinking add returns a Future for the handler.
  • Shares one bloc instance across tests instead of building a new one in build.
  • Puts mocktail verify calls inside act, before the states were compared.
open as a page

In a Flutter widget test, how do you replace a real Bloc with bloc_test's MockBloc and drive its states with whenListen?

level: middleimportance: should knowfreq 40%

basics

~10 s

Declare 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.

open as a page

With bloc_test, how do you test a Bloc event handler registered with a debounce or restartable event transformer?

level: middleimportance: should knowfreq 25%

basics

~20 s

Pass wait with at least the debounce window so blocTest sleeps before closing the bloc, add several events in quick succession, and expect only the state the surviving event produces; for restartable, expect only the latest request's states.

open as a page

A blocTest fails although the printed expected and actual CartState lists look identical; what is bloc_test telling you, and how do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The states compare by identity: CartState does not implement value equality, so equal-looking instances are unequal. bloc_test notices the printed forms match and adds a warning; extend Equatable with every field in props, or assert with matchers.

open as a page

In bloc_test's blocTest, what do seed and skip each do, and why can a seeded test correctly expect an empty list?

level: seniorimportance: should knowfreq 30%

basics

~20 s

seed emits a starting state on the bloc before blocTest subscribes, so it is never recorded; skip drops the first N states recorded after that. Once seeded, emitting a state equal to the current one is ignored, so a no-op event records nothing.

open as a page