关于 testing
面向 Dart 和 Flutter 开发者的测试技能,将 VGV 约定应用于单元测试和 Widget 测试。内容涵盖可读结构、测试隔离、mocktail、Widget 辅助工具、Golden 测试及测试运行指导。
主要功能
- 01描述性分层测试命名
- 02隔离测试设置并使用私有 Mock
- 03基于 mocktail 的 Mock 规范
- 04使用 pumpApp 编写 Widget 测试
- 05Golden 测试标记与视觉检查
- 06test MCP 工具运行指引
适用场景
- 01组织 Dart 单元测试
- 02编写侧重行为的 Widget 测试
- 03减少测试间共享状态
- 04在 Flutter monorepo 中运行测试
MCP 相关
技能包含通过 test MCP 工具运行测试的指引,包括 directory 和 timeout 选项。文档将 mcp__very-good-cli__test 列为 Claude Code 的选项,并说明服务端不可用时的命令行替代方案。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill testing安装后在 Claude Code 中输入/testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/animation-testing.mdconfiguration.mdcoverage.mdgolden-tests.mdmatchers.mdmocktail.mdtest-patterns.mdwidget-tests.md详细介绍
Dart & Flutter 测试
Dart 和 Flutter 的 VGV 测试约定。假定你已熟悉框架 API;以下内容是在这些 API 之上采用的团队风格。
核心标准
所有测试工作均遵循以下标准:
- 使用描述性测试名称 — 名称应详尽、易读,并描述行为;不要使用
'works'或'renders' - 采用层级化的 group/test 结构,使描述读起来像自然句子 — 顶层
group对应类,嵌套group对应方法,test对应行为(例如:UserRepository→getUser→returns User when API succeeds) - 使用字符串插值引用类型 — 使用
'returns $User',而不是'returns User',这样重命名会自动同步 - 每个文件使用私有 mock — 声明
class _MockX extends Mock implements X {},使用下划线前缀以避免文件间耦合 - 在 group 内设置测试环境 — 所有
setUp/tearDown调用都应放在group内,绝不要放在main()顶层 - 在
setUp()中使用late初始化可变对象 — 声明late MyDep dep;,然后在setUp中赋值,确保每个测试都获得全新实例 - 测试间不共享可变状态 — 绝不要使用会在测试间持续存在的静态成员、全局变量或顶层 final 实例
- 使用
package:mocktail— 绝不要使用package:mockito - 使用常量测试标签 — 使用带有
static const字段的abstract class TestTag;绝不要将原始字符串字面量作为标签传入 - 测试行为,而不是属性 — Widget 测试应关注功能结果;静态视觉属性通过 golden 测试验证
- 使用
pumpApp测试辅助方法 — 通过共享辅助方法test/helpers/pump_app.dart封装 Widget;绝不要内联使用pumpWidget(MaterialApp(...)) - 为所有 golden 测试添加标签 — 标注
TestTag.golden,以便独立运行或更新 golden - 当项目不在工作区根目录时,为
testMCP 工具传入directory— Flutter 项目位于子目录(例如mobile/)中的 monorepo 需要设置directory: 'mobile';只有当pubspec.yaml位于工作区根目录时才省略该参数 - 为
testMCP 工具传入timeout_seconds— 未设置超时的pumpAndSettle()可能导致 Flutter 测试无限挂起;请设置上限(例如timeout_seconds: 120),以便终止运行,而不是一直等待 testMCP 工具的跨执行环境回退方案 — 在 Claude Code 中使用mcp__very-good-cli__test;若主机没有此插件的 Bash hooks,且未连接该 MCP 服务,则直接运行very_good test(或flutter test/dart test),并使用相同的覆盖率和超时选项;绝不要因缺少 MCP 服务而阻塞
文件组织
| 约定 | 规则 |
|---|---|
| 文件后缀 | 每个测试文件均以 _test.dart 结尾 |
| 目录 | 所有测试均位于 test/ 下 |
| 镜像结构 | test/ 与 lib/ 完全对应 — lib/src/models/user.dart → test/src/models/user_test.dart |
| 辅助文件 | 共享测试工具放在 test/helpers/ 中(例如 pump_app.dart、fakes.dart) |
Group 和 Test 层级
组织 group,使拼接后的描述读起来像自然句子。顶层 group 中应直接使用 PascalCase 类型本身,而不是字符串。
class _MockApiClient extends Mock implements ApiClient {}
void main() {
group(UserRepository, () {
late ApiClient apiClient;
late UserRepository subject;
setUp(() {
apiClient = _MockApiClient();
subject = UserRepository(apiClient: apiClient);
});
group('getUser', () {
test('returns $User when API call succeeds', () async {
when(() => apiClient.fetchUser(any()))
.thenAnswer((_) async => User(id: '1', name: 'Dash'));
final result = await subject.getUser('1');
expect(result, equals(User(id: '1', name: 'Dash')));
verify(() => apiClient.fetchUser('1')).called(1);
});
test('throws $UserNotFoundException when API returns 404', () {
when(() => apiClient.fetchUser(any()))
.thenThrow(ApiException(statusCode: 404));
expect(
() => subject.getUser('1'),
throwsA(isA<UserNotFoundException>()),
);
});
});
});
}
命名约定
| 模式 | 示例 |
|---|---|
| 返回值 | 'returns $User when API call succeeds' |
| 抛出异常 | 'throws $UserNotFoundException when user is not found' |
| 调用依赖项 | 'calls apiClient.deleteUser with correct id' |
| 发出状态 | 'emits [loading, success] when data is fetched' |
| 条件行为 | 'returns cached value when cache is not expired' |
| 边界情况 | 'returns empty list when repository has no items' |
测试隔离
每项测试都必须能够单独运行,且无论以何种顺序或并行运行都能通过。使用
--test-randomize-ordering-seed random 来暴露隐藏的依赖关系。
| 反模式 | 问题 | 正确做法 |
|---|---|---|
main() 顶层的 setUp | 测试运行器合并文件进行优化时会出错 | 将 setUp 移入 group 中 |
顶层的 final dep = _MockDep(); | 所有测试共享同一个实例;状态会泄漏 | 在组内使用 late 和 setUp |
class MockDep extends Mock(公开) | 其他测试文件可以导入并依赖它 | 使用 class _MockDep extends Mock(私有) |
| 静态/全局可变变量 | 状态会在测试间保留 | 在 setUp 中重置,或完全避免使用 |
| 必须按特定顺序运行的测试 | 脆弱,随机排序时会失败 | 让每项测试完全自包含 |
测试私有逻辑
不要直接测试私有方法。通过使用该私有逻辑的公有方法来测试私有逻辑:
// If _normalizeEmail is private, test it through the public createUser method:
test('normalizes email to lowercase before saving', () async {
when(() => repository.save(any())).thenAnswer((_) async {});
await subject.createUser(email: '[email protected]');
final captured = verify(() => repository.save(captureAny())).captured;
expect(captured.first.email, equals('[email protected]'));
});
Widget 测试
| 规则 | 详情 |
|---|---|
使用 testWidgets | 每项 Widget 测试都使用 testWidgets,而不是 test |
优先使用 find.byType | 默认查找器;查找用户可见内容时使用 find.text,仅在类型/文本不明确时使用 find.byKey |
| 按行为类别分组 | 使用 renders、navigates、calls [MethodName]、updates 作为嵌套组名称 |
| 关注行为 | 断言 Widget 的行为(显示文本、调用回调、导航);使用 golden 测试检查视觉外观 |
| 模拟 Blocs 和 Cubits | 使用 package:bloc_test 中的 MockBloc/MockCubit;不要在 Widget 测试中提供真实的 Bloc |
优先使用 pump() 而非 pumpAndSettle() | 对于 CircularProgressIndicator 等无限动画,pumpAndSettle 会一直挂起 |
pumpApp 辅助工具
每项 Widget 测试都通过一个共享辅助工具包装待测 Widget,因此没有测试会内联自己的
MaterialApp:
// test/helpers/pump_app.dart
extension PumpApp on WidgetTester {
Future<void> pumpApp(Widget widget) {
return pumpWidget(
MaterialApp(
home: widget,
),
);
}
}
将其从 test/helpers/helpers.dart 导出,这样每项测试只需一行即可导入:
import '../helpers/helpers.dart';
void main() {
group(MyWidget, () {
testWidgets('renders greeting text', (tester) async {
await tester.pumpApp(const MyWidget());
expect(find.text('Hello'), findsOneWidget);
});
});
}
Widget 测试反模式
| 反模式 | 问题 | 正确做法 |
|---|---|---|
在每个测试中内联使用 MaterialApp | 样板代码重复;设置不一致 | 使用 pumpApp 辅助函数 |
默认使用 find.byKey 查找器 | 测试与实现中的键耦合 | 优先使用 find.byType 或 find.text |
| 测试内边距、颜色或字体大小 | 脆弱;每次设计调整都会导致测试失败;断言外观而非行为 | 使用 golden 测试进行视觉验证 |
交互后未调用 pump() | Widget 树不会重建;断言看到的是过期状态 | 在 tap、enterText 等操作后始终调用 pump() |
| 在 Widget 测试中使用真实 Bloc | 测试会变成集成测试;速度慢、脆弱且难以隔离 | 使用 bloc_test 中的 MockBloc/MockCubit |
内边距、背景颜色和字体大小是反复出现的诱惑。在 Widget 测试中断言这些内容会让测试与设计系统耦合,并在每次重新设计时失败——应将它们移至标记为 TestTag.golden 的 golden 测试中。
其他资源
- references/mocktail.md — Mock 和 Fake、桩设置、参数匹配器、验证、回退值
- references/test-patterns.md —
setUp/tearDown生命周期,以及异步、流、异常、Equatable和回调的模式 - references/widget-tests.md — 完整的 Bloc 支持的 Widget 测试、
pumpApp中的主题/本地化、pump 方法、查找器和交互 - references/golden-tests.md — golden 文件测试(设置、编写 golden、标记、运行/更新、反模式)
- references/matchers.md — 匹配器速查
- references/configuration.md —
dart_test.yaml配置(标签、平台覆盖)以及通过 MCPtest工具运行测试 - references/coverage.md — 覆盖率模式和包/导入参考
- references/animation-testing.md — 隐式/显式动画、AnimatedSwitcher、页面过渡和注入的控制器的测试