关于 testing
这项测试技能整理了 TypeScript 测试的组织、断言命名及测试与基准测试的区分规范,也提供类型测试、测试设置、审查和精简测试的指引。
主要功能
- 01Result 断言辅助工具
- 02区分测试与基准测试
- 03测试文件文档规范
- 04按行为拆分测试文件
- 05基于行为的测试命名
适用场景
- 01为 wellcrafted Result 编写测试
- 02将性能报告与 CI 测试分开
- 03按不同测试行为拆分文件
- 04审查或精简现有测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill testing安装后在 Claude Code 中输入/testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/honest-tests.mdsetup-pattern.mdtest-deletion-grill.mdtest-structure.mdtype-testing.md详细介绍
测试文件约定
参考资料
根据当前工作内容按需查阅:
- 如果正在处理负向类型测试(
@ts-expect-error、bun:test类型测试策略、不使用as any),请阅读 references/type-testing.md - 如果正在处理测试设置架构(
setup()模式、可组合的设置、避免使用beforeEach、共享 schema),请阅读 references/setup-pattern.md - 如果正在处理测试组织结构(扁平测试、
describe()边界、优先使用辅助函数而非嵌套),请阅读 references/test-structure.md - 如果正在审查现有测试,检查是否存在模棱两可的断言、透传 getter、停滞的 fake、无用的 fake 接口,或与代码相矛盾的文档字符串,请阅读 references/honest-tests.md
- 如果正在删除或精简测试,而这些测试可能没有保留价值,请阅读 references/test-deletion-grill.md
外部阅读资料:
- Kent C. Dodds,"避免在测试中嵌套":优先使用设置函数而非
beforeEach,采用扁平测试 - Kent C. Dodds,"AHA 测试":避免在测试中匆忙进行抽象
- Kent C. Dodds,Testing JavaScript:测试对象工厂模式
- Matt Pocock,"如何测试类型":使用 vitest 的
expectTypeOf进行类型测试 - Matt Pocock,
shoehorn:便于编写测试的部分 mock
相关技能:请参阅
services-layer,了解正在测试的服务模式。请参阅typescript,了解类型测试约定。
结果断言
当测试断言 wellcrafted Result 时,请使用 wellcrafted/testing 中的 expectOk 和 expectErr,而不要手动编写错误检查。
import { expectErr, expectOk } from 'wellcrafted/testing';
const data = expectOk(await service.doThing());
expect(data.id).toBe('1');
const error = expectErr(await service.doThing({ invalid: true }));
expect(error.name).toBe('InvalidInput');
当值是 wellcrafted Result 时,避免克隆本地辅助函数、使用 expect(error).toBeNull() 检查成功,以及通过 if (error) throw ... 解包。
此规则不适用于普通响应正文、UI 快照或其他仅包含 error 属性的对象。
测试与基准测试
两种不同的文件扩展名,对应两种不同的用途:
*.test.ts:使用expect()断言行为。在bun test下运行(仓库默认配置,CI)。没有至少一次expect()调用的测试文件不应使用此扩展名。*.bench.ts:测量并报告。输出表格、耗时或存储大小。仅在bun bench下运行。无需断言(共享硬件上的性能阈值容易波动;建议关注可视化趋势)。
单个文件只能属于其中一种,不能兼具两者。基准测试位于包内的 src/__benchmarks__/ 下;测试则与所覆盖的模块放在一起。bun test 的默认发现 glob 只会匹配 *.test.ts 及同类文件,因此将报告文件从 .test.ts 重命名为 .bench.ts,即可将其排除在 CI 之外。
文件级文档注释
每个 .test.ts 文件都必须以一个 JSDoc 块开头,说明测试内容以及验证的关键行为。这是对模块契约的说明。
结构
/**
* [Module Name] Tests
*
* [1-3 sentences explaining what this file tests and why these tests matter.]
*
* Key behaviors:
* - [Behavior 1]
* - [Behavior 2]
* - [Behavior 3]
*
* See also:
* - `related-file.test.ts` for [related aspect]
*/
良好示例
/**
* Cell-Level LWW CRDT Sync Tests
*
* Verifies cell-level LWW conflict resolution where each field
* has its own timestamp. Unlike row-level LWW, concurrent edits to
* DIFFERENT fields merge independently.
*
* Key behaviors:
* - Concurrent edits to SAME field: latest timestamp wins
* - Concurrent edits to DIFFERENT fields: BOTH preserved (merge)
* - Delete removes all cells for a row
*/
不良示例(过于简略)
// Tests for create-tables
章节标题
对于较长的测试文件(100 行以上),请使用注释标题分隔逻辑章节:
// ============================================================================
// MESSAGE_SYNC Tests
// ============================================================================
按多个方面拆分测试文件
如果一个模块包含多个不同的行为方面,请拆分为多个专注的测试文件,而不要全部放在一个庞大的文件中:
| 模式 | 使用场景 |
|---|---|
{module}.test.ts | 核心 CRUD 行为、正常路径、边缘情况 |
{module}.types.test.ts | 类型推断验证、负向类型测试 |
{module}.{scenario}.test.ts | 特定场景(CRDT 同步、离线、集成) |
何时拆分
- 文件超过约 500 行
- 测试涵盖真正不同的关注点(CRUD 与同步、类型)
- 不同关注点需要不同的设置
何时不拆分
- 拆分后会产生少于 3 个测试的文件
- 所有测试共用相同的设置并关注同一事项
测试命名
测试描述必须是行为断言,而不是含糊的描述。测试失败时,名称应能说明出错之处。
规则
- 描述发生了什么,不要写“应该正常工作”或“处理正确”
- 测试边界情况时,包含相应条件
- 不要使用填充词:“应该”“正确地”“恰当地”都没有意义
好的名称
test('upsert stores row and get retrieves it', () => { ... });
test('filter returns only published posts', () => { ... });
test('concurrent edits to different fields: both preserved', () => { ... });
test('delete vs update race: update wins (rightmost entry)', () => { ... });
test('observer fires once per transaction, not per operation', () => { ... });
test('get() throws for undefined tables with helpful message', () => { ... });
不好的名称
test('should work correctly', () => { ... }); // What works? What's correct?
test('should handle batch operations', () => { ... }); // Handle how?
test('basic test', () => { ... }); // Says nothing
test('should create and retrieve rows correctly', () => { ... }); // Vague "correctly"
模式:{action} {outcome} [condition]
"upsert stores row and get retrieves it"
^^^^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^
action outcome action outcome
"observer fires once per transaction, not per operation"
^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
subject outcome condition
"get() returns not_found for non-existent rows"
^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
action outcome condition