测试助手
- 作者仓库星标 0
- 作者更新于 实时读取
- 作者仓库 skills-registry
- 领域
- 工程开发
- 兼容 Agent
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- 信任分
- 88 / 100 · 社区维护
- 作者 / 版本 / 许可
- @tomevault-io · 未声明 license
- Token 消耗评级
- 低消耗
- 接入复杂程度
- 需简单配置
- 是否需要外部 API Key
- 不需要
- 兼容的系统
- 未声明(默认跨平台)
- 底层运行要求
- 无特殊要求
- 文件与系统权限
-
- 只读
- 允许写入 / 修改
- 网络行为
- 允许外网请求
- 安装命令数
- 26 条
档案由构建时根据 SKILL.md 与安装命令自动衍生,可能与作者实际意图存在差异。
需要注意: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: flutter-bloc-testing
description: Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for…
category: 工程开发
runtime: 无特殊运行时
---
# flutter-bloc-testing 输出预览
## PART A: 任务判断
- 适用问题:代码实现、重构、调试或代码审查。
- 输入要求:目标材料、限制条件、期望输出和验收方式。
- 证据边界:围绕“Contents / The blocTest helper / Mocking repositories with mocktail”读取原文规则,不把推断写成作者承诺。
## PART B: 执行结果
- **01** 任务判断:确认你的需求是否属于代码实现、重构、调试或代码审查,并标出输入、限制和预期结果。
- **02** 执行计划:优先按“Contents / The blocTest helper / Mocking repositories with mocktail”拆成步骤,说明每一步会读取什么、修改什么、产出什么。
- **03** 交付结果:给出可复制的命令、文件改动、检查清单或内容草稿,并说明如何继续迭代。
- **04** 风险边界:结合 读取文件、写入/修改文件、会按任务需要访问外部网络、通常不需要额外 API Key 给出执行前确认项。
## Running Rules
- 读取文件、写入/修改文件;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先小样例验证,再放大到真实任务。
- 交付时同时给结果、检查口径和下一步迭代建议。 原文没有稳定的斜杠命令要求。安装验证后通常全局生效,直接在对话里点名这个 Skill 并描述任务即可。
告诉 Agent 目标文件或材料、期望结果、不可改范围、是否允许联网或执行命令。本 Skill 的权限画像是:读取文件、写入/修改文件。
先用一个小任务确认它会围绕“Contents / The blocTest helper / Mocking repositories with mocktail”工作;涉及文件或命令时,先看 diff、日志、预览或测试结果。
检查最终产物是否包含明确结果、必要证据和下一步动作;如果输出泛泛而谈,就补充输入、边界和验收标准后重跑。
---
name: flutter-bloc-testing
description: Writes deterministic unit tests for Blocs using `bloc_test` and `mocktail`, including tests for…
category: 工程开发
source: tomevault-io/skills-registry
---
# flutter-bloc-testing
## 什么时候使用
- 用于组织测试、定位失败并形成修复闭环 适合处理工程开发场景下的代码实现、调试、重构、测试或代码审查,核心价值是把输入、判断、执行、验证和交付边界固定下来,避免 Agent 泛泛回答。 把任务拆成可执行、可检查、可继续迭代的步骤;通常不需…
- 面向代码实现、重构、调试或代码审查,优先处理能明确输入、步骤和验收标准的工作。
## 需要提供什么
- 目标材料、目录范围、期望结果和不可改动内容。
- 是否允许联网、执行命令、读写文件或调用外部服务。
## 执行规则
- 围绕「Contents / The blocTest helper / Mocking repositories with mocktail」组织步骤,不把推断写成作者事实。
- 读取文件、写入/修改文件;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先跑小样例,确认结果可检查后再扩大任务范围。
## 输出要求
- 给出最终产物、关键证据、验证方式和下一步动作。
- 信息不足时标记 unknown,不编造命令、平台或依赖。 作者原文负责流程事实;仓库文件负责来源和命令;流狐只补充适用场景、限制和质量判断。
skill "flutter-bloc-testing" {
输入层 -> 用户目标 + 目标文件 + 禁止范围 + 验收标准
上下文层 -> Contents / The blocTest helper / Mocking repositories with mocktail
规则层 -> SKILL.md 触发条件 / 执行顺序 / 输出格式
运行层 -> 无特殊运行时 | 读取文件、写入/修改文件 | 会按任务需要访问外部网络
安全层 -> 通常不需要额外 API Key + 小任务验证 + diff / 日志复核
输出层 -> 可复制结果 + 检查清单 + 下一步迭代
} 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.
先判断是否适合
作者设计意图
作者的方法与取舍
边界和复核