mcpField
T
testing

testing

github.com/verygoodopensource测试与质量17595 次安装3 天前更新
代码仓库

关于 testing

面向 Dart 和 Flutter 开发者的测试技能,将 VGV 约定应用于单元测试和 Widget 测试。内容涵盖可读结构、测试隔离、mocktail、Widget 辅助工具、Golden 测试及测试运行指导。

主要功能

  1. 01描述性分层测试命名
  2. 02隔离测试设置并使用私有 Mock
  3. 03基于 mocktail 的 Mock 规范
  4. 04使用 pumpApp 编写 Widget 测试
  5. 05Golden 测试标记与视觉检查
  6. 06test MCP 工具运行指引

适用场景

  1. 01组织 Dart 单元测试
  2. 02编写侧重行为的 Widget 测试
  3. 03减少测试间共享状态
  4. 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.yaml
references/
animation-testing.md
configuration.md
coverage.md
golden-tests.md
matchers.md
mocktail.md
test-patterns.md
widget-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
  • 当项目不在工作区根目录时,为 test MCP 工具传入 directory — Flutter 项目位于子目录(例如 mobile/)中的 monorepo 需要设置 directory: 'mobile';只有当 pubspec.yaml 位于工作区根目录时才省略该参数
  • 为 test MCP 工具传入 timeout_seconds — 未设置超时的 pumpAndSettle() 可能导致 Flutter 测试无限挂起;请设置上限(例如 timeout_seconds: 120),以便终止运行,而不是一直等待
  • test MCP 工具的跨执行环境回退方案 — 在 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 测试中。

其他资源

评论

支持 Markdown