flutter-bloc-testing
- Repo stars 0
- Author updated Live
- Author repo skills-registry
- Domain
- Engineering
- Compatible agents
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- Trust score
- 88 / 100 · community maintained
- Author / version / license
- @tomevault-io · no license declared
- Token usage
- Lean
- Setup complexity
- Guided setup
- External API key
- Not required
- Operating systems
- Unspecified (assume cross-platform)
- Runtime requirements
- No special requirements
- Permissions
-
- Read-only
- Write / modify
- Network behavior
- External requests
- Install commands
- 26 variants
Profile is derived at build time from SKILL.md and install vectors. Subject to drift from author intent.
Heads up: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: flutter-bloc-testing
description: Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for…
category: engineering
runtime: no special runtime
---
# flutter-bloc-testing output preview
## PART A: Task fit
- Use case: Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for `bloc_concurrency` transformers and `HydratedBloc` persistence. Use when adding tests for a Bloc's event handlers, verifying its emitted state sequence, mocking a Repository dependency, or asserting that `restartable()` cancels in-flight requests. Prerequisite: `flutter-bloc-setup`, `flutter-bloc-feature-pattern`, and `flutter-bloc-async-api`. Use when this capability is needed..
- Inputs: target material, constraints, expected output, and acceptance criteria.
- Evidence boundary: follow “Contents / The blocTest helper / Mocking repositories with mocktail” and do not present inference as author intent.
## PART B: Execution result
- **01** The card summarizes the use case; runtime output centers on “Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for `bloc_concurrency` transformers and `HydratedBloc` persistence. Use when adding tests for a Bloc's event handlers, verifying its emitted state sequence, mocking a Repository dependency, or asserting that `restartable()` cancels in-flight requests. Prerequisite: `flutter-bloc-setup`, `flutter-bloc-feature-pattern`, and `flutter-bloc-async-api`. Use when this capability is needed.”.
- **02** When the source has headings, the agent prioritizes “Contents / The blocTest helper / Mocking repositories with mocktail” so the result follows the author’s structure.
- **03** Typical output includes task judgment, concrete steps, required commands or file edits, validation, and follow-up options.
- **04** Risk context follows the fingerprint: read files, write/modify files; may access external network resources; usually needs no extra API key.
## Running Rules
- read files, write/modify files; may access external network resources; usually needs no extra API key.
- Validate with a small sample before expanding scope.
- Return the result, validation criteria, and next iteration options. The source does not require a stable slash command. After installation, invoke the skill by name and describe the task.
Name target files or source material, expected output, forbidden changes, and whether network or shell access is allowed. Permission fingerprint: read files, write/modify files.
Start with a small task and check whether the result follows “Contents / The blocTest helper / Mocking repositories with mocktail”. Inspect diffs, logs, previews, or tests before expanding scope.
Confirm the final output includes a concrete result, evidence, and next action. If it stays generic, tighten inputs, boundaries, and acceptance criteria.
---
name: flutter-bloc-testing
description: Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for…
category: engineering
source: tomevault-io/skills-registry
---
# flutter-bloc-testing
## When to use
- Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for `bloc_concurrency` tra…
- Use it when the task has clear inputs, repeatable steps, and validation criteria.
## What to provide
- Target material, scope, expected result, and forbidden changes.
- Whether network, commands, file writes, or external services are allowed.
## Execution rules
- Organize steps around “Contents / The blocTest helper / Mocking repositories with mocktail” and keep inference separate from source facts.
- read files, write/modify files; may access external network resources; usually needs no extra API key.
- Validate with a small sample before expanding the task.
## Output requirements
- Return the deliverable, key evidence, validation method, and next action.
- Mark missing information as unknown; do not invent commands, platforms, or dependencies. The author source anchors workflow facts; repository files anchor sources and commands; Fluxly only adds fit, limitations, and quality judgment.
skill "flutter-bloc-testing" {
input -> user goal + target files + boundaries + acceptance criteria
context -> Contents / The blocTest helper / Mocking repositories with mocktail
rules -> SKILL.md triggers / order / output contract
runtime -> no special runtime | read files, write/modify files | may access external network resources
guardrails -> usually needs no extra API key + small-sample validation + diff/log review
output -> copyable result + checklist + next iteration
} Testing Blocs
Drives Blocs from outside with deterministic inputs and asserts the exact state sequence they emit. bloc_test wraps the lifecycle (build a fresh Bloc, dispatch events, await stream, dispose) into a single declarative call. mocktail provides null-safe Repository mocks without code generation. Concurrency tests use a Completer to control timing so an assertion can run between a stale and a fresh request. Builds on flutter-bloc-async-api; the example suite tests RestaurantListBloc from that skill.
Contents
- The
blocTesthelper - Mocking repositories with mocktail
- Testing concurrency transformers
- Testing HydratedBloc persistence
- Workflow: Test a Bloc
- Applied to Talabat-clone
- Examples
The blocTest helper
blocTest<B, S>(description, ...) registers a Dart test that builds a fresh Bloc, runs an action, and asserts the emitted states. It is a structured wrapper around expectLater(bloc.stream, emitsInOrder([...])) with five named hooks:
| Hook | Purpose |
|---|---|
build |
Construct a fresh Bloc. Called once per test. |
setUp |
Configure mocks before build. Use to wire when(() => repo.fetch()).thenAnswer(...). |
seed |
Inject an initial state without going through the constructor. Useful for "given the Bloc is in state X, when event Y arrives..." |
act |
Dispatch events. The body is (bloc) => bloc.add(...). |
wait |
Optional Duration to wait after act before asserting. Needed only when handlers use timers. |
expect |
List of states the Bloc must emit, in order. Either literal values or matchers (isA<Loading>()). |
verify |
Optional final hook for assertions on mocks: verify(() => repo.fetch(loc)).called(1). |
A passing blocTest is proof that for these exact inputs, the Bloc emits exactly this sequence. The Bloc's initial state is implicit and not asserted — expect: describes the deltas.
Mocking repositories with mocktail
mocktail generates mocks at runtime via class MockX extends Mock implements X {}. No build_runner, no generated files. Two patterns matter:
Stubbing a return value:
when(() => repo.fetchNearbyStores(any()))
.thenAnswer((_) async => const Result.success(<Store>[]));
any() matches any positional argument. For named arguments use any(named: 'foo'). For matching specific values pass the value directly.
Fallback values for unhandled types:
If a method takes a custom object as a parameter, register a fallback once in setUpAll:
setUpAll(() {
registerFallbackValue(const Location(lat: 0, lng: 0));
});
Without the fallback, any() against a Location parameter throws a StateError at first use.
Testing concurrency transformers
restartable() says "cancel the in-flight handler when a new event arrives". A naive test that dispatches two events back-to-back and asserts the second result will pass even without restartable() — because the events run synchronously fast enough that the first never overlaps with the second.
The reliable test uses a Completer to pin the first handler in flight while the second arrives:
test('refresh cancels the in-flight initial load', () async {
final initialCompleter = Completer<Result<List<Store>>>();
final refreshCompleter = Completer<Result<List<Store>>>();
final calls = <Completer<Result<List<Store>>>>[initialCompleter, refreshCompleter];
when(() => repo.fetchNearbyStores(any()))
.thenAnswer((_) => calls.removeAt(0).future);
final bloc = RestaurantListBloc(repo: repo);
bloc.add(const RestaurantListLoadRequested(_loc));
await Future<void>.delayed(Duration.zero); // let _onLoad start
bloc.add(const RestaurantListRefreshRequested(_loc));
await Future<void>.delayed(Duration.zero); // let restartable() cancel
// `_storeA`/`_storeB` are runtime finals (not `const`), so `Result.success([...])`
// cannot be `const` either.
initialCompleter.complete(Result.success([_storeA])); // dropped — restartable cancelled this handler's emitter
refreshCompleter.complete(Result.success([_storeB]));
await bloc.close();
// The first emit was Loading, then Loading again from the refresh,
// then Loaded with the second result. The first Result.success([_storeA])
// is dropped because the handler that would emit it was cancelled.
expect(bloc.state, isA<RestaurantListLoaded>());
expect((bloc.state as RestaurantListLoaded).stores, [_storeB]);
});
The Completer-based timing is what proves restartable() is actually doing its job. Without it the test only proves "events serialize", which they would anyway under sequential() or even concurrent() for this simple case.
Testing HydratedBloc persistence
HydratedBloc reads and writes a global HydratedBloc.storage singleton. Test files that touch HydratedBlocs must register an in-memory Storage fake once per test process, or tests will pollute each other (and the device, if you forget to swap to a fake).
class _InMemoryStorage implements Storage {
final _map = <String, dynamic>{};
// `dynamic` matches the upstream `Storage` interface — a real fake can tighten it.
@override
dynamic read(String key) => _map[key];
@override
Future<void> write(String key, dynamic value) async => _map[key] = value;
@override
Future<void> delete(String key) async => _map.remove(key);
@override
Future<void> clear() async => _map.clear();
// The official `Storage` interface declares four required members
// (read/write/delete/clear). `close()` isn't in the contract today,
// but a no-op makes the fake forward-compatible if a future version
// adds it.
@override
Future<void> close() async {}
}
setUpAll(() {
HydratedBloc.storage = _InMemoryStorage();
});
setUp(() async {
// `clear()` returns a Future. With the in-memory fake the mutation lands
// synchronously before the first await, but always `await` it so the pattern
// survives a swap to an async-yielding fake (e.g. a SharedPreferences-backed one).
await HydratedBloc.storage.clear();
});
Asserting rehydration is then a two-Bloc dance: build, mutate, close, build a second instance, check state. The firstWhere call subscribes to the stream before add enqueues the handler (microtask order), so the persisted write happens before close().
test('CartBloc rehydrates after restart', () async {
final a = CartBloc(repo: repo);
a.add(CartItemAdded(_product, 2));
await a.stream.firstWhere((s) => s is CartLoaded); // subscribes before handler runs
await a.close();
// Second instance reads the persisted state synchronously in its constructor.
final b = CartBloc(repo: repo);
expect(b.state, isA<CartLoaded>());
expect((b.state as CartLoaded).items.length, 1);
});
Workflow: Test a Bloc
Task Progress
- Step 1 — Mirror the bloc path. Test file lives at
test/ui/features/<feature>/bloc/<feature>_bloc_test.dart. - Step 2 — Add dev deps.
flutter pub add --dev bloc_test mocktail. - Step 3 — Declare mock classes.
class MockStoreRepository extends Mock implements StoreRepository {}at the top of the file. - Step 4 —
setUpAllfor fallbacks. RegisterregisterFallbackValue(<custom-type>(...))for every non-primitive type used inany(). - Step 5 —
setUpper test. Instantiate the mock, instantiate the Bloc. - Step 6 — Write
blocTestcases. One per "given X event(s), expect Y state sequence". Always cover: happy path, failure path, and any branch in the handler. - Step 7 — Cover transformer behavior. For each
restartable()/droppable()/sequential()handler, write one explicit test that proves the transformer's contract (useCompleterfor timing). - Step 8 — If
HydratedBloc. Add the_InMemoryStoragefake insetUpAll, clear it insetUp, and write a rehydration round-trip test. - Step 9 — Feedback loop.
flutter test test/ui/features/<feature>/bloc/<feature>_bloc_test.dart→ if a test hangs, the Bloc is probably awaiting a future that never completes (checkCompleterusage) → if a test reports an unexpected extra state, the previous test's mock leaked (checksetUpscoping).
Applied to Talabat-clone
Two tests from the cart suite illustrate the patterns.
CartBloc rehydrates after cold start (PRD_states.md §7).
The cart is Cart: active → converted | abandoned. On cold start, a returning user must see their cart exactly as they left it. The test seeds _InMemoryStorage, builds a fresh CartBloc, and asserts the initial emitted state already has the persisted items — no Loading flicker.
CartBloc rejects items from a second store (PRD_catalog.md §15.5).
The one-cart / one-store rule says adding a product from a different store should transition to CartStoreSwitchPending(pendingProduct) rather than blend the carts. The test seeds a CartLoaded(items: [productFromStoreA]), dispatches CartItemAdded(productFromStoreB, 1), and asserts the next state is CartStoreSwitchPending, not CartLoaded with both items. A follow-up test dispatches CartStoreSwitchConfirmed and asserts the cart is replaced.
These tests are the only safety net between the PRD rules and a future refactor that silently drops one of them.
Examples
A test suite for RestaurantListBloc from flutter-bloc-async-api. Covers success, failure, and the restartable refresh transformer.
test/ui/features/restaurant_list/bloc/restaurant_list_bloc_test.dart
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/core/result/failure.dart';
import 'package:my_app/core/result/result.dart';
import 'package:my_app/data/repositories/store_repository.dart';
import 'package:my_app/domain/models/location.dart';
import 'package:my_app/domain/models/store.dart';
import 'package:my_app/ui/features/restaurant_list/bloc/restaurant_list_bloc.dart';
import 'package:my_app/ui/features/restaurant_list/bloc/restaurant_list_event.dart';
import 'package:my_app/ui/features/restaurant_list/bloc/restaurant_list_state.dart';
class MockStoreRepository extends Mock implements StoreRepository {}
const _loc = Location(lat: 25.276987, lng: 55.296249); // Dubai
final _storeA = Store(id: 'a', name: 'A', deliveryMinutes: 20);
final _storeB = Store(id: 'b', name: 'B', deliveryMinutes: 30);
void main() {
setUpAll(() {
// `_loc` is `const` — same canonical instance used by both registration and dispatch.
registerFallbackValue(_loc);
});
group('RestaurantListBloc', () {
late MockStoreRepository repo;
setUp(() {
repo = MockStoreRepository();
});
blocTest<RestaurantListBloc, RestaurantListState>(
'emits [Loading, Loaded] when fetch succeeds',
setUp: () {
when(() => repo.fetchNearbyStores(any()))
.thenAnswer((_) async => Result.success([_storeA, _storeB]));
},
build: () => RestaurantListBloc(repo: repo),
act: (b) => b.add(const RestaurantListLoadRequested(_loc)),
expect: () => [
const RestaurantListState.loading(),
RestaurantListState.loaded([_storeA, _storeB]),
],
verify: (_) {
verify(() => repo.fetchNearbyStores(_loc)).called(1);
},
);
blocTest<RestaurantListBloc, RestaurantListState>(
'emits [Loading, Empty] when fetch returns no stores',
setUp: () {
when(() => repo.fetchNearbyStores(any()))
.thenAnswer((_) async => const Result.success(<Store>[]));
},
build: () => RestaurantListBloc(repo: repo),
act: (b) => b.add(const RestaurantListLoadRequested(_loc)),
expect: () => [
const RestaurantListState.loading(),
const RestaurantListState.empty(),
],
);
blocTest<RestaurantListBloc, RestaurantListState>(
'emits [Loading, Failure] on network error',
setUp: () {
when(() => repo.fetchNearbyStores(any()))
.thenAnswer((_) async => const Result.failure(Failure.network()));
},
build: () => RestaurantListBloc(repo: repo),
act: (b) => b.add(const RestaurantListLoadRequested(_loc)),
expect: () => [
const RestaurantListState.loading(),
const RestaurantListState.failure('No internet connection'),
],
);
// This test drops out of `blocTest` and uses raw `test(...)` because
// `blocTest`'s `wait:` parameter is a fixed `Duration` — it cannot interleave
// `Completer.complete` calls between event dispatches, which is what proves
// the `restartable()` contract.
test('refresh cancels the in-flight initial load', () async {
// Named completers so we can resolve them in a deliberate order.
final initialCompleter = Completer<Result<List<Store>>>();
final refreshCompleter = Completer<Result<List<Store>>>();
final completers = [initialCompleter, refreshCompleter];
// First call to `fetchNearbyStores` returns initialCompleter.future,
// the second returns refreshCompleter.future.
when(() => repo.fetchNearbyStores(any()))
.thenAnswer((_) => completers.removeAt(0).future);
final bloc = RestaurantListBloc(repo: repo);
final emitted = <RestaurantListState>[];
final sub = bloc.stream.listen(emitted.add);
// Dispatch the initial load; let _onLoad start awaiting the first future.
bloc.add(const RestaurantListLoadRequested(_loc));
await Future<void>.delayed(Duration.zero);
// Dispatch refresh; restartable() cancels the initial handler's emitter.
bloc.add(const RestaurantListRefreshRequested(_loc));
await Future<void>.delayed(Duration.zero);
// Now resolve the FIRST completer. Its emit must be dropped because the
// initial handler's emitter is already done.
initialCompleter.complete(Result.success([_storeA]));
refreshCompleter.complete(Result.success([_storeB]));
// One more turn so the refresh handler emits its Loaded.
await Future<void>.delayed(Duration.zero);
await bloc.close();
await sub.cancel();
// The load-bearing invariant: we never emit `Loaded([_storeA])`.
// Only one `Loaded` should appear, and it must carry the refresh's result.
final loadedStates = emitted.whereType<RestaurantListLoaded>().toList();
expect(loadedStates, hasLength(1),
reason: 'restartable() should drop the stale Loaded([_storeA])');
expect(loadedStates.single.stores, [_storeB]);
expect(bloc.state, isA<RestaurantListLoaded>());
});
});
}
For features that extend HydratedBloc (e.g., CartBloc), add the in-memory storage fake from the "Testing HydratedBloc persistence" section above to the top-level setUpAll, and clear it in setUp so tests do not see each other's persisted state.
Source: abdallhMoukdad/flutter-bloc-skills — distributed by TomeVault.
Decide Fit First
bloc_testandmocktail, including tests forbloc_concurrencytransformers…Design Intent
How To Use It
Boundaries And Review