skip to content

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%

answer

  1. seed runs before the subscription
  2. skip counts recorded states
  3. equal state after an emit is dropped
  4. seed bypasses the event handlers

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.

solid answer

~40 s

`seed` is a function returning a state. blocTest calls `bloc.emit(seed())` right after `build` and only then subscribes to `bloc.stream`, so the seeded state becomes `bloc.state` but never appears in the recorded list. `skip` (default `0`) drops the first N states recorded after the subscription - useful to assert only the tail of what `act` produced, at the cost of leaving those states unchecked. Because the seed already counts as an emission, a later `emit` of a state equal to the current one is silently dropped: seed a two-item cart, remove an item id that is not in it, and the correct expectation is `const <CartState>[]`. Seeding is fast but bypasses the handlers, so it can create states the bloc could never reach on its own.

go deeper

for a junior

Remember that seed sets the starting state and never shows up in expect, while skip ignores the first recorded states.

for a middle

Explain the order: seed is emitted before the subscription, skip applies to the stream afterwards. Walk through why an equal state is dropped after a seed.

for a senior

Use seed to test no-op events and point out when a seeded state is unreachable. Argue for full state lists over skip unless the skipped states are covered elsewhere.

for a principal

Weigh seed-based speed against end-to-end handler coverage in a team's test conventions, and when seeded tests hide real regressions.

## Two parameters that look alike Both `seed` and `skip` let a **blocTest** case ignore something, which is why they get confused. They act at different moments of the run: - **`seed`** - an optional `State Function()`. blocTest calls `build`, then emits the seeded state directly on the instance, then subscribes to its stream. - **`skip`** - an optional `int`, default `0`. blocTest subscribes with `bloc.stream.skip(skip)`, so the first N states emitted **after** the subscription are discarded before they reach the recorded list. The seeded state is therefore never in the list at all - `skip` does not need to, and cannot, remove it. | | seed | skip | |---|---|---| | When it acts | before the subscription | on the recorded stream | | What it changes | the bloc's current `state` | which recorded states are compared | | Appears in `expect`? | never | the skipped ones never do | | Main risk | a state the bloc cannot really reach | unverified intermediate states | ## Why seed exists Many behaviours only matter from a particular state: removing an item needs a cart with items, clearing needs a non-empty cart. Without `seed` you would add setup events in `act` and then `skip` their states. `seed` jumps straight there: ```dart blocTest<CartBloc, CartState>( 'emits an empty cart when CartCleared is added', build: CartBloc.new, seed: () => const CartState(items: [apple, pear]), act: (bloc) => bloc.add(const CartCleared()), expect: () => const [CartState(items: [])], ); ``` `emit` on a bloc is marked `@protected` and `@visibleForTesting` in bloc 9 - blocTest is exactly the kind of caller it is meant for. Note that seeding goes through `emit` directly, not through an event handler, so it does not produce a `Transition` the way a handled event does. ## Why a seeded test can expect nothing The bloc package drops an emission when the new state `==` the current one **and** the instance has already emitted once. The one exception is the very first emission, which is allowed even when it equals the initial state. The seed is that first emission. After it: 1. `act` adds `CartItemRemoved('banana')` to a cart holding apple and pear. 2. The handler builds a new `CartState` with the same items and calls `emit`. 3. With value equality (for example `Equatable`), the new state equals the current one, so the emission is dropped. 4. Nothing is recorded, and `expect: () => const <CartState>[]` is the correct assertion. This makes `seed` a clean way to test "this event is a no-op here". If the states do **not** implement value equality, every new instance is different and the same test records one state - a sign that equality, not the handler, is wrong. ## When skip is the better tool - The intermediate states are already covered by another test and you only care about the final one. - A handler emits a loading state and then a result, and this case is about the result. The cost is real: skipped states are never checked, so a regression in them passes silently. Prefer asserting the whole list unless it is genuinely noise. ## Pitfalls - **Unreachable seeds.** A seeded state the handlers could never produce (a cart with a negative total) makes the test prove behaviour that cannot happen in the app. - **Seed plus skip confusion.** `skip: 1` after a seed does not remove the seed; it removes the first state `act` produced. - **Relying on a seed to hide setup bugs.** If setting up the state through events is part of the behaviour, drive it through `act` instead.

  • If CartState does not implement value equality, what does the seeded 'remove a missing item' test record?
    One state. Without `==` overridden, the new `CartState` instance is not equal to the seeded one, so the bloc emits it and blocTest records it. The test expecting an empty list fails, which points at missing value equality rather than at the handler.
  • When is driving the setup through act better than using seed?
    When the path to the state is itself behaviour worth covering, or when a seed could produce a state the handlers never reach. Adding the setup events in `act` exercises the real handlers; you then either assert every state or `skip` the setup ones deliberately.

saying these in an interview costs you the question

  • Lists the seeded state first in expect, as though blocTest recorded it.
  • Uses skip: 1 to remove the seeded state from the recorded list.
  • Thinks a no-op event on a seeded bloc must still record one state.
  • Treats seed as running the event handlers that would produce that state.
  • Skips intermediate states by habit, leaving loading states untested.