skip to content

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

level: middleimportance: should knowfreq 25%

answer

  1. wait: a real Duration
  2. close() cancels in-flight handlers
  3. rapid events, one resulting state
  4. superseded request emits nothing
  5. inject the debounce duration

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.

solid answer

~40 s

blocTest closes the bloc soon after `act`, and `Bloc.close()` cancels in-flight handlers, so a handler still waiting on a debounce window or a slow call never gets its states recorded - or, if the transformer flushes on close, the test passes without proving the delay. `wait` fixes that: a `Duration` blocTest sleeps for, in real time, after `act` and before `close()`. To prove the transformer rather than just the handler, add several `CartQuantityChanged` events in a row and expect exactly one resulting state; for a `restartable()` handler, add two requests where the first is slower and expect only the second's states, because the cancelled handler's later `emit` calls are ignored. Inject the debounce duration through the constructor so the suite does not sleep for production-length windows.

code

dart · 18 lines
dart
import 'package:bloc/bloc.dart';
import 'package:rxdart/rxdart.dart';

EventTransformer<E> debounce<E>(Duration duration) {
  return (events, mapper) => events.debounceTime(duration).switchMap(mapper);
}

class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc({this.quantityDebounce = const Duration(milliseconds: 300)})
      : super(const CartState()) {
    on<CartQuantityChanged>(
      (event, emit) => emit(state.withQuantity(event.id, event.quantity)),
      transformer: debounce(quantityDebounce),
    );
  }

  final Duration quantityDebounce;
}

go deeper

for a junior

Recall that blocTest has a wait parameter for handlers that emit later, such as debounced ones.

for a middle

Explain why close() loses late states and how wait, several rapid events and one expected state together prove a debounce.

for a senior

Design restartable and droppable tests with controllable stubs, and keep windows injectable so the suite stays fast and deterministic.

for a principal

Decide where timing behaviour is tested - unit tests of the transformer versus bloc tests - and what that costs a large suite in wall-clock time.

## Why transformers need special handling In bloc 9, `on<E>(handler, transformer: ...)` lets you control how events of one type reach their handler. A **debounce** transformer waits until events stop arriving for a window before letting the last one through; **bloc_concurrency** (0.3) supplies `sequential()`, `concurrent()`, `droppable()` and `restartable()`. What each strategy means is covered where transformers are taught; the testing problem is timing. **blocTest** runs `act`, yields once so queued work runs, and then calls `close()`. In the bloc source, `Bloc.close()` closes the event stream, **cancels every in-flight handler's emitter** and waits for them to finish. A cancelled emitter silently ignores later `emit` calls. So: - a handler still inside a debounce window, or still awaiting a slow repository call, loses the states it would have emitted; - a transformer that flushes a pending event when its source closes (rxdart's `debounceTime` does by default) may still deliver the event during `close()` - the test can pass while proving nothing about the window. Either way, a test without enough time is not testing the transformer. ## The wait parameter `wait` is an optional `Duration`. blocTest awaits `Future.delayed(wait)` after `act` and before closing. It is **real time**, not a fake clock, so: 1. Set it to at least the debounce window. 2. Keep the window injectable (a constructor parameter) so tests can use a short one. 3. Avoid production-length windows in the suite; every test pays the full sleep. ## Proving a debounce A single event plus `wait` proves only that the handler works. To prove the debounce: - add several events in quick succession in `act`; - expect exactly **one** state, built from the **last** event; - optionally add a second test where `act` awaits longer than the window between two events and expect **two** states. `act` may be `async`; blocTest awaits it, so `await Future<void>.delayed(...)` between `add` calls is allowed. ## Proving restartable or droppable For `restartable()`, each new event cancels the handler still running for the previous one: 1. Stub the repository so the first call completes after the second (for example with two `Completer`s, completing the second first). 2. Add two `CartTotalsRequested` events in `act`. 3. Expect only the states of the second request. The first handler's later `emit` is ignored because its emitter was cancelled. For `droppable()`, the mirror image holds: events arriving while a handler runs are ignored, so you expect only the first request's states. Remember that `close()` cancels in-flight work too - give slow stubs time to resolve with `wait`, or complete them inside `act`. | Transformer | Test shape | Expected states | |---|---|---| | debounce | several rapid events, `wait` >= window | one, from the last event | | restartable | two requests, first slower | only the second request's | | droppable | two requests while first runs | only the first request's | | sequential | two requests | first's states, then second's | ## Pitfalls - **No `wait`.** The test either records nothing or passes by accident through a flush on close. - **`wait` shorter than the window.** Same failure, intermittently. - **A test that adds one event.** It cannot tell a debounced handler from a plain one. - **Fake clocks.** blocTest's `wait` uses real timers; do not assume a widget test's fake time advances it.

  • Why can a debounce test without wait still pass?
    Because `Bloc.close()` closes the event stream, and rxdart's `debounceTime` flushes a pending event when its source closes by default. The handler can then emit during `close()` and the state is recorded, so the test passes without ever proving that the handler waited for the window.
  • Can a widget test's fake clock replace blocTest's wait?
    Not for blocTest itself: `wait` awaits a real `Future.delayed` inside an ordinary test. The practical way to keep the suite fast is to inject a short window into the bloc and set `wait` to cover it.

saying these in an interview costs you the question

  • Tests a debounced handler with one event and calls the transformer covered.
  • Assumes blocTest waits for pending events on its own before comparing.
  • Thinks close() lets every in-flight handler finish and emit.
  • Hard-codes a production-length debounce window into every test.
  • Expects both requests' states from a restartable handler.