关于 writing-unit-tests
面向编写或修改单元测试的工程师,强调验证行为契约而非实现细节。内容涵盖确定性测试、清晰命名,以及对失败路径的有效断言。
主要功能
- 01测试公开行为而非实现
- 02每个测试聚焦一个具名行为
- 03编写无休眠的确定性测试
- 04有选择地模拟外部边界
- 05断言结果与失败路径
适用场景
- 01为修改后的单元编写测试
- 02替换耦合内部实现的测试
- 03排查异步测试的不稳定问题
- 04增加错误行为覆盖
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ riekelt/ principal-engineer --skill writing-unit-tests安装后在 Claude Code 中输入/writing-unit-tests确认已加载。
详细介绍
编写单元测试
必备背景: principal-engineering 技能。testing-changes 规定一次变更需要哪些测试;本技能讲的是如何编写测试。
概述
单元测试是一项带有名称的行为断言,下一位工程师会在构建失败时阅读它。核心原则:测试契约,而非实现。 名称承载断言,测试应足够简单,以免测试本身出错。
契约优先于实现
- 通过单元的公开契约进行测试。保留行为的重构不应导致测试失败;若确实失败,说明测试断言的是实现。
- 不要断言调用顺序、内部状态,或方法 A 调用了方法 B,除非该交互本身就是契约(边界上必须发生的副作用)。
- 绝不要从生产代码的算术逻辑推导预期值,既不要重新实现公式,也不要调用计算该值的共享辅助函数。预期值应是独立推算出的字面量(手动计算、根据规范或真实数据得出),并在注释中说明推导过程。
每个测试只测一种行为,并以断言命名
- 每个测试只测一种行为;拆分测试比事后分析一个有多项断言的失败更省力。
- 名称应说明对象、场景和预期结果:
expired_token_is_rejected_with_401,而非test_auth_3。测试名称应描述行为、状态转换和不变量;绝不要描述交付顺序、工单编号或阶段。 - 按清晰可见的顺序编排、执行、断言。测试中不要包含分支、循环或逻辑:包含逻辑的测试本身就需要测试。共享设置可使用构建器或按角色命名的测试夹具;用途不明的测试夹具会掩盖断言所依赖的安排事实。生成和迭代应放在构建器和辅助函数中,而非测试主体中。基于属性的测试是测试不变量的合适形式,并应遵循其框架的结构;基于示例的测试应保持无逻辑。
确定性
- 单元测试中不要使用真实时间、真实随机性、真实网络或真实文件系统:注入时钟、固定随机种子或注入随机数,并模拟边界。
- 不要使用休眠。等待异步工作时,应基于条件(在截止时间前轮询可观察到的结果),而不是等待固定时长。
- 不稳定的测试就是失败的测试:修复它,或明确隔离并注明负责人(参见
testing-changes中的失败测试规则);反复运行直到通过,就是在屏蔽检测器。
模拟即假设
- 模拟不由你掌控的边界(网络、时钟、文件系统、第三方服务);对于单元范围内由你掌控的代码,优先使用真实协作者。对于包裹非自有资源的自有封装(例如连接数据库的仓库类),应在自有代码最后接触非自有资源的接缝处进行模拟,并始终使用按角色命名且清晰可见的测试数据。每个模拟都硬编码了对契约的一项假设;模拟过时,会让测试套件在真实集成已经损坏时仍然全绿。
- 如果测试大部分内容都是模拟连接,它测的就是模拟。要么扩大单元范围,纳入真实行为;要么接受这个接缝需要集成测试(并说明是哪种测试)。
- 测试夹具是对现实的标注快照:应尽可能精简,按其在场景中的作用命名,契约变更时有意更新,绝不要盲目重新生成来让失败的测试转绿。
断言与失败路径
- 用值断言结果,而非断言没有抛出异常。“没有抛出异常”几乎不能说明任何问题。
- 失败路径也是一等测试对象:应暴露带类型的失败、大声进入降级模式,并确保保护措施确实有效(参见
handling-failures)。 - 测试本身也应遵循不静默吞错契约:测试代码中不得捕获并忽略错误,也不得使用在前置条件缺失时静默跳过的条件断言。无法运行的测试必须失败,或明确跳过并说明原因。
常见错误
- 镜像测试:重新实现生产逻辑来计算预期结果。
- 模拟回音室:模拟自己的类,然后断言该模拟。
- 超大型测试:包含十二条断言,却只有一个名称,无法得知哪条断言失效。
- 共享可变测试夹具导致测试顺序影响结果;每个测试都应构建或接收自己的状态。
- 用 sleep “修复”不稳定测试,只是让问题不那么常见。
- 让测试夹具通过检查:修改预期值以匹配实际输出,却不推导新值为何正确。
评论
支持 Markdown