skip to content

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%

answer

  1. same text, different identity
  2. WARNING about Equatable in the failure
  3. props must list every field
  4. isA with having as fallback

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.

solid answer

~40 s

blocTest compares the recorded list with `expect` using `==`. If `CartState` neither extends `Equatable` nor overrides `==` and `hashCode`, two instances with the same items are different objects, so the match fails. When the string forms of both lists are identical, bloc_test appends a warning asking you to extend `Equatable`, override `==` and `hashCode`, or implement `Comparable`, or to use matchers. The fix is value equality with **every** field in `props`; a field left out makes different states compare equal, so tests pass falsely and the bloc drops real changes. If a state cannot have value equality, assert with `isA<CartState>().having((s) => s.items.length, 'items', 1)`. When the strings differ and `expect` returned a list of states, bloc_test prints a diff instead.

code

dart · 8 lines
dart
blocTest<CartBloc, CartState>(
  'emits a cart with one item when CartItemAdded is added',
  build: CartBloc.new,
  act: (bloc) => bloc.add(const CartItemAdded(apple)),
  expect: () => [
    isA<CartState>().having((s) => s.items.length, 'items.length', 1),
  ],
);

go deeper

for a junior

Remember that Dart compares objects by identity unless the class defines value equality, which is why states usually extend Equatable.

for a middle

Explain both bloc_test messages: the Equatable warning when the printed lists match, and the diff when they differ.

for a senior

Diagnose the props trap and in-place mutation from test output alone, and choose between value equality and matchers deliberately.

for a principal

Set team rules for state classes, such as generated or linted equality and review of props, so equality bugs surface in tests rather than in production.

## How blocTest compares states At the end of a **blocTest** case, the recorded list of states is matched against whatever `expect` returns. A plain `List` of states is compared element by element with `==`. Dart's default `==` on a class is **identity**: two objects are equal only if they are the same instance. A handler always builds a new `CartState`, and the test builds its own expected `CartState`, so without value equality they can never be equal - however similar they look. ## Reading the two failure messages bloc_test (version 10) adds help to the failure in two different situations: - **The strings match but the objects do not.** blocTest compares `'$states'` with `'$expected'`. If the text is identical yet the match failed, it appends a warning: ensure the state instances extend `Equatable`, override `==` and `hashCode`, or implement `Comparable` - or use matchers in `expect` instead of concrete instances. This is the "looks identical but fails" case. - **The strings differ.** When `expect` returned a `List` of states, blocTest appends a coloured diff between expected and actual, so you can see which field or which state is off. Note that the text comparison depends on `toString`. With `Equatable`, `stringify` defaults to true in debug mode, so states print their props; a class with no `toString` prints `Instance of 'CartState'` for every state, which also produces identical strings. ## Fix 1: value equality The usual fix is to make the state a value type, most often with the **equatable** package (version 3): ```dart class CartState extends Equatable { const CartState({this.items = const []}); final List<CartItem> items; @override List<Object?> get props => [items]; } ``` `Equatable` compares `props` element by element and compares nested lists and maps by content, so two carts with equal items are equal. `CartItem` needs value equality too, or the list comparison falls back to identity for its elements. ## The props trap A field missing from `props` is worse than no equality at all: 1. Add a `discountCode` field to `CartState` but forget it in `props`. 2. A test expecting `CartState(items: [apple], discountCode: 'SAVE10')` passes against a state with no code - the field is simply not compared. 3. In the app, the bloc drops an emission whose only change is the code, because the new state `==` the current one. So the test passes falsely **and** the UI never updates. Review `props` whenever a state gains a field. ## Fix 2: matchers When a state cannot have value equality (it wraps a type you do not control, or you only care about one field), return matchers from `expect`: - `isA<CartState>()` - asserts only the type. - `isA<CartState>().having((s) => s.items.length, 'items.length', 1)` - asserts one derived value. - A list of such matchers still enforces order and count. Matchers make tests less brittle but also less strict: a field you did not mention is not checked. ## A related symptom: a missing state If a handler mutates the current state's list in place and emits the same instance, the new state is identical to the current one; once the bloc has emitted at least once (bloc lets only the very first emission through when it equals the current state), it drops it. The test then fails because a state is **missing**, not because equality is wrong. The cure is immutable states - build a new list instead of `state.items.add(...)`. ## Checklist - Identical printed lists plus a failure means identity comparison. - Every field goes in `props`, including nested value types. - Use matchers when value equality is not possible. - A missing state after an in-place mutation is a state-design bug, not a test bug.

  • Why does a field missing from Equatable props break the app as well as the test?
    The bloc drops any emission whose state `==` the current one. If the only change is in a field outside `props`, the new state compares equal, so the change is never emitted and widgets never rebuild. The same blind spot lets a test pass against a state with the wrong value in that field.
  • A seeded CartBloc's handler calls state.items.add(item) on a growable list and then emit(state). What does blocTest show?
    No new state for that event, provided the bloc has already emitted once (only the very first emission passes even when equal). The emitted object is the current instance, so it is equal to itself and the bloc drops it. The test fails with a missing state; the fix is to emit a new state built from a new list, not to change the test.

saying these in an interview costs you the question

  • Assumes Dart compares objects by their field values by default.
  • Loosens expect to isA<CartState>() everywhere instead of adding value equality.
  • Leaves a newly added field out of props and trusts the passing tests.
  • Believes a test comparing identical-looking states must be a bloc_test bug.
  • Mutates the state's list in place and expects a new state to be emitted.