关于 grove-testing
为 Grove 开发者提供测试策略指导,帮助判断该测什么以及如何评估测试价值。它侧重贴近用户行为的集成测试,并涵盖测试分层、边界条件和回归测试。
主要功能
- 01按用户行为规划测试
- 02优先编写集成测试
- 03评估测试有效性
- 04覆盖信任边界与错误情况
- 05将缺陷转化为回归测试
适用场景
- 01为新功能挑选测试范围
- 02审查现有测试的价值
- 03为缺陷修复补充测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ autumnsgrove/ lattice --skill grove-testing安装后在 Claude Code 中输入/grove-testing确认已加载。
详细介绍
Grove 测试技能
何时启用
在以下情况下启用此技能:
- 决定要测试什么(而不只是如何测试)
- 为 Grove 的新功能编写测试
- 审查现有测试是否有效
- 在没有具体指导的情况下被要求“添加测试”
- 评估测试是否真正发挥了作用
- 重构导致大量测试失败(这是测试质量差的表现)
对于技术实现(Vitest 语法、模拟模式、断言),请同时使用 javascript-testing 技能。
测试理念
“编写测试。不要太多。以集成测试为主。” — Guillermo Rauch
这概括了 Grove 对测试的全部看法:
编写测试——自动化测试很有价值。它们让你可以放心重构,充当文档,并在用户发现回归问题之前将其捕获。
不要太多——测试的收益会递减。目标不是覆盖率数字,而是信心。当你有信心发布时,测试就足够了。
以集成测试为主——集成测试能捕获真实问题,又不易脆弱。它们测试用户实际体验到的行为,而不是内部实现。
指导原则
“测试越接近软件的实际使用方式,就越能让你有信心。” — Kent C. Dodds(Testing Library)
问问自己:功能失效时,这个测试会失败吗? 如果会,它就有价值。如果它只会在重构时失败,那它测试的就是实现细节。
什么样的测试有价值
好的测试具有以下特性(Kent Beck 的 Test Desiderata):
| 特性 | 含义 |
|---|---|
| 对行为敏感 | 实际功能失效时会失败 |
| 不受结构影响 | 安全重构时不会失败 |
| 确定性 | 每次结果都相同,不会时好时坏 |
| 快速 | 几秒内而不是几分钟内提供反馈 |
| 诊断清晰 | 失败时,你能准确知道哪里出了问题 |
| 编写成本低 | 投入与代码复杂度成正比 |
信心测试
编写测试前,先问:
- 如果它在生产环境中失效,我会注意到吗? 如果会,就测试它。
- 功能失效时,这个测试会失败吗? 如果不会,就不要编写。
- 这个测试是否接近用户与该功能的交互方式? 如果不是,请重新考虑。
不要测试什么
并非所有内容都需要测试。有些测试反而会损害代码库。
跳过测试
| 内容 | 原因 |
|---|---|
| 简单代码 | getter、setter、没有逻辑的数据模型 |
| 框架行为 | 相信 SvelteKit 的路由能够正常工作 |
| 实现细节 | 内部状态、私有方法、CSS 类 |
| 一次性脚本 | 维护成本超过其价值 |
| 不稳定的原型 | 需求尚不明确,代码也会发生变化 |
少量测试
| 内容 | 方法 |
|---|---|
| 配置 | 做冒烟测试以确认能够加载,而非测试每个选项 |
| 第三方集成 | 在边界处模拟,测试代码的响应 |
| 视觉设计 | 使用快照测试或视觉回归测试,而非单元测试 |
充分测试
| 内容 | 原因 |
|---|---|
| 业务逻辑 | 应用的核心价值 |
| 面向用户的流程 | 用户实际体验的内容 |
| 边界情况 | 错误状态、空状态、边界条件 |
| 错误修复 | 每个错误都应转化为测试,以防止回归 |
测试奖杯
现代 JavaScript 测试遵循测试奖杯,而不是旧的测试金字塔:
╭─────────╮
│ E2E │ ← Few: critical user journeys
╰────┬────╯
╭─────────┴─────────╮
│ Integration │ ← Many: this is where confidence lives
╰─────────┬─────────╯
╭──────┴──────╮
│ Unit │ ← Some: pure functions, algorithms
╰──────┬──────╯
╭──────────┴──────────╮
│ Static Analysis │ ← TypeScript, ESLint (always on)
╰─────────────────────╯
各层的作用
静态分析(TypeScript、ESLint)
- 捕获拼写错误、类型错误和明显失误
- 零运行时开销,始终运行
- 这是你的第一道防线
单元测试
- 纯函数、算法、工具函数
- 速度快、隔离性强、易于调试
- 不要什么都模拟——实际可行时,测试真实行为
集成测试(最佳着力点)
- 多个单元协同工作
- 测试用户实际体验的行为
- 比单元测试更不易脆弱,比端到端测试更快
- 大多数测试都应该属于这一类
端到端测试(Playwright)
- 仅覆盖关键用户旅程:登录、结账、核心流程
- 编写和维护成本高
- 留给失败会影响业务的流程
编写有效的测试
结构:准备-执行-断言
每个测试都应遵循以下模式:
it("should reject invalid email during registration", async () => {
// Arrange: Set up the scenario
const invalidEmail = "not-an-email";
// Act: Do the thing
const result = await registerUser({ email: invalidEmail, password: "valid123" });
// Assert: Check the outcome
expect(result.success).toBe(false);
expect(result.error).toContain("email");
});
执行部分应只有一行。如果不止一行,这个测试可能做得太多了。
命名:说清楚什么会出错
测试名称应描述行为,而不是实现:
好的名称:
should reject registration with invalid emailshould show error message when API failsshould preserve draft when navigating away
不好的名称:
test email validation(具体是什么?)handleSubmit works(“正常工作”是什么意思?)test case 1(不行)
一次只测试一件事
每个测试都应当只有一个失败原因。测试失败时,你应该能立刻知道哪里出了问题。
// Bad: Testing multiple things
it('should handle registration', async () => {
// Tests validation, API call, redirect, AND email sending
});
// Good: Focused tests
it('should reject invalid email format', ...);
it('should call API with valid data', ...);
it('should redirect after successful registration', ...);
it('should send welcome email after registration', ...);
测试信任边界(Rootwork)
每个信任边界都应针对有效和无效数据编写测试:
表单操作: 提交缺失字段、错误类型和边界值 → 验证 parseFormData() 返回结构化错误(而不是崩溃)
KV 读取: 模拟 KV 返回损坏或过时的 JSON → 验证 safeJsonParse() 会回退到默认值
缓存读取: 模拟缓存服务返回意外的数据形态 → 验证 createTypedCacheReader() 会使用回退值
Catch 块: 触发重定向和 HTTP 错误 → 验证 isRedirect()/isHttpError() 会正确处理它们
参考:Rootwork(@autumnsgrove/lattice/server)提供 parseFormData、safeJsonParse、createTypedCacheReader、isRedirect 和 isHttpError。
实践集成测试
集成测试是 Grove 测试策略的核心。下面介绍如何编写优质的集成测试。
测试用户行为,而非实现
// Bad: Testing implementation
it("should set isLoading state to true", async () => {
const { component } = render(LoginForm);
await fireEvent.click(getByRole("button"));
expect(component.isLoading).toBe(true); // Testing internal state!
});
// Good: Testing user experience
it("should show loading indicator while logging in", async () => {
render(LoginForm);
await fireEvent.click(getByRole("button", { name: /sign in/i }));
expect(getByRole("progressbar")).toBeInTheDocument();
});
使用无障碍查询
像用户查找元素那样查询元素:
// Priority order (best to worst):
getByRole("button", { name: /submit/i }); // How screen readers see it
getByLabelText("Email"); // Form fields
getByText("Welcome back"); // Visible text
getByTestId("login-form"); // Last resort
不要过度模拟
模拟会降低对集成的信心。请谨慎使用:
// Over-mocked: False confidence
vi.mock("./api");
vi.mock("./validation");
vi.mock("./utils");
// You're testing... nothing real
// Better: Mock at boundaries
vi.mock("./external-api"); // Mock the network, not your code
// Let validation, utils, etc. run for real
经验法则: 如果你在模拟自己编写的内容,不妨重新考虑。
测试失败时
测试失败是在告诉你一些事情。请认真对待。
良性失败(符合预期)
- 功能发生变化 — 测试发现行为变了。更新测试。
- 错误已修复 — 旧测试有误。修正它。
- 需求发生变化 — 测试反映的是旧需求。更新测试。
不良失败(测试质量不佳的表现)
- 重构了内部代码 — 测试与实现耦合。重写它。
- 更改了 CSS 类 — 测试在查询实现细节。改用无障碍查询。
- 调整了代码顺序 — 测试依赖执行顺序。让它不受顺序影响。
如果重构经常导致测试失败,说明你的测试测错了内容。
Bug → 测试流程
每个生产环境中的 Bug 都应该转化为一个测试:
- 报告 Bug — 用户无法使用某些商品结账
- 在本地复现 — 找出确切的条件
- 编写失败测试 — 捕获 Bug 出现的条件
- 修复 Bug — 测试现在可以通过
- 测试防止回归 — Bug 永远不会再出现
这是最有价值的测试实践之一。它能将痛苦转化为保护。
应避免的反模式
冰淇淋筒
╭───────────────────────────╮
│ Many E2E tests │ ← Slow, brittle, expensive
╰───────────┬───────────────╯
╭─────┴─────╮
│ Few int. │
╰─────┬─────╯
╭───┴───╮
│ Few │
│ unit │
╰───────╯
这完全本末倒置。端到端测试成本高昂。集成测试的投资回报率最高。
测试实现细节
// Testing implementation (bad)
expect(component.state.items).toHaveLength(3);
expect(handleClick).toHaveBeenCalledWith({ id: 1 });
// Testing behavior (good)
expect(getByRole("list").children).toHaveLength(3);
expect(getByText("Item added!")).toBeInTheDocument();
覆盖率表演
一味追求 100% 的覆盖率会导致糟糕的测试:
// Written only to hit coverage, provides zero value
it("should have properties", () => {
const user = new User();
expect(user.email).toBeDefined();
expect(user.name).toBeDefined();
});
覆盖率是信号,而不是目标。糟糕测试带来的高覆盖率,比良好测试带来的中等覆盖率更糟。
滥用快照
快照适用于:
- 复杂的序列化输出
- 错误消息格式
- API 响应结构
快照不适用于:
- UI 组件(每次样式变更都会导致失败)
- 带有时间戳或随机 ID 的内容
- 大型对象(没人会审查 500 行的快照差异)
Grove 测试工作流
收到添加测试的请求时,请遵循此工作流:
1. 理解功能
此功能为用户做什么?不是如何实现,而是它能提供什么价值?
2. 找出关键路径
如果此功能失效,会有什么问题?这些就是你的测试用例。
3. 先编写集成测试
从能验证真实用户行为的测试开始。只有复杂逻辑才需要添加单元测试。
4. 让测试靠近代码
src/
└── lib/
└── features/
└── auth/
├── login.ts
├── login.test.ts ← Right next to the code
└── register.ts
5. 持续运行测试
npx vitest # Watch mode during development
npx vitest run # CI verification
快速决策指南
| 情况 | 操作 |
|---|---|
| 新功能 | 为面向用户的行为编写集成测试 |
| 修复 Bug | 先编写能复现 Bug 的测试,然后再修复 |
| 重构 | 运行现有测试;如果安全的变更会导致测试失败,那这些测试就有问题 |
| “需要更高覆盖率” | 为尚未覆盖的行为添加测试,而不是为尚未覆盖的代码行添加测试 |
| 纯函数/算法 | 为其编写单元测试 |
| API 端点 | 使用模拟的外部服务进行集成测试 |
| UI 组件 | 使用 Testing Library 进行组件测试 |
| 关键用户流程 | 使用 Playwright 进行端到端测试 |
与其他技能集成
javascript-testing
以下情况请使用 javascript-testing:
- Vitest 配置语法
- 模拟模式和 API
- 断言参考
- SvelteKit 专用测试模式
grove-documentation
编写测试描述时,请遵循 Grove 的风格:
- 名称清晰、直接
- 不使用术语
- 描述用户会遇到什么
code-quality
编写测试前后都要运行代码检查和类型检查。静态分析能发现与测试不同的 Bug。
自查清单
在认为测试“完成”之前:
- 测试描述的是用户行为,而不是实现方式
- 每个测试都有一个明确的失败原因
- 测试使用无障碍查询(getByRole、getByLabelText)
- 仅在外部边界使用模拟
- 测试名称说明了测试失败时会出什么问题
- 不对易变内容进行快照测试
- Bug 修复包含回归测试
- 测试运行迅速(几秒,而不是几分钟)
好的测试能让你充满信心地发布。这就是全部意义所在。