mcpField
T
testing-on-the-toilet

testing-on-the-toilet

github.com/shamashel测试与质量3656 次安装9 周前更新
代码仓库

关于 testing-on-the-toilet

将 Google Testing on the Toilet 的测试原则整理为测试编写与审查指南,面向软件开发者。内容强调验证可观察行为、选择合适测试层级与测试替身,并检查测试能否发现真实缺陷。

主要功能

  1. 01识别只复述实现的测试
  2. 02按风险选择测试层级
  3. 03按需选择真实对象或测试替身
  4. 04编写清晰且聚焦的断言
  5. 05确保测试隔离且确定
  6. 06验证测试能暴露缺陷

适用场景

  1. 01审查现有测试是否验证行为
  2. 02为新功能选择测试层级
  3. 03减少脆弱的模拟对象测试
  4. 04检查测试的确定性与有效性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/shamashel/testing-on-the-toilet --skill testing-on-the-toilet

安装后在 Claude Code 中输入/testing-on-the-toilet确认已加载。

附带文件references/episodes.md

详细介绍

厕所里的测试

摘编自 Google 的 Testing on the Toilet 系列文章(2007–2026)。文章索引:references/episodes.md。

每次新增测试和评审测试时,都运行这一套流程。完成标准:每个合入的测试都能在实际行为出现 bug 时失败,并能在不改变行为的重构后继续通过。

1. 变更检测器关卡

变更检测器只会复述生产代码的结构(尤其是按顺序 verify 每个协作者的调用),却不检查可观察结果。正确和错误的实现都能通过;任何重命名、提取或调整顺序都会导致整套测试失败。

改写测试,断言状态、返回值、渲染输出或持久化数据。如果说不出用户可见的契约,就删除测试并说明原因。不要通过机械更新几十个镜像测试来“修复”重构。

只有当调用本身就是契约时,才允许交互式 verify(例如,只发送一次邮件、不重复扣费、presenter 告知 view 显示 X)。如果不确定,就改为断言状态或返回值;如果做不到,就停下来询问该测试保护的是哪种可观察结果。

2. 选择仍能忠实反映行为的最便宜层级

先说出 bug 或风险。选择表格中最上方能捕获它的层级。只有当更便宜的层级无法捕获该风险时,才升级到更高层级(并说明会漏掉什么)。如果两个层级都能捕获风险,就用 SMURF(速度、可维护性、资源利用/成本、可靠性、忠实度)来权衡:优先选择更便宜、更快、更可靠的方案。

风险层级
纯逻辑、解析、校验、reducer通过公开 API进行快速单元测试
你拥有的进程内协作者真实对象或由所有者维护的 fake
服务/HTTP 契约所有者提供的 fake 或隔离式服务器——不要手写请求 mock
UI 连线问题(禁用、未绑定、隐藏)操作渲染后的控件(点击/输入),不要直接调用处理器
跨系统关键路径少量 e2e 测试:每个用例一条路径,加上关键错误类别;断言系统结果,而不是文案/布局

在叠加多个层级之前,先集思广益,列出关键风险。覆盖率指标能发现缺口,但不能证明质量。要覆盖分支的两侧(隐式的 else 也算)。不要穷举所有组合;提取谓词并分别覆盖。

优先通过公开 API进行测试。为调用方永远不会传入的输入穷举私有辅助函数的测试,实质上仍是变更检测器。

3. 选择测试替身

顺序:真实对象 → fake → stub → mock。mock 放在最后。

  • 如果真实对象速度快、结果确定且在进程内运行,就用真实对象。
  • Fake:由你(或库的所有者)维护的简化可用实现。保持范围精简。如果 fake 会被多个地方共用,就针对真实对象进行契约测试。
  • Stub:为查询提供预设返回值。不要对 getter 使用 verify。
  • Mock:验证副作用调用,且该交互本身就是规范。

硬性规则:

  • 不要 mock 你不拥有的类型。封装第三方 API;mock 这个封装层;再用真实库测试该封装层。
  • mock 超过一两个协作者,或出现很长的 when/verify 链,是 seam 异味——提取更精简的端口,并为它创建 fake。
  • 在构造函数中注入长期存在的协作者;将每次调用的工作作为方法参数传入。不要在被测逻辑中使用隐藏的单例、无法 mock 的静态方法或 now()。

4. 编写测试(DAMP)

在测试中,DAMP(描述性且有意义的短语)胜过 DRY。测试本身没有测试——读者必须能在方法体中看到原因与结果相邻。

  • 每个测试只验证一种行为。 命名为 unit_scenario_expectedOutcome(或项目中的等效形式)。testFoo 不算名称。
  • 在一个代码块中按 准备–执行–断言 编写。远离断言、内容混杂的共享 @Before,会让错误的预期值悄悄混进来。
  • 辅助方法隐藏 无关的 构造细节。与场景相关的字段应在测试中 清晰可见。带默认值的构建器/工厂是好做法;默默断言辅助方法的默认值则不是。
  • 使用 字面量 输入和预期输出。不要用可能与生产代码共享同一错误的循环/条件语句来计算预期结果。如果辅助方法必须包含逻辑,就为它编写单元测试。
  • 精确断言:只断言待测字段,而不是整个对象/屏幕截图/哈希顺序。对于常见的正常对象,一次完整相等性检查就够了。
  • 每个输入使用 不同的非默认值(0、""、第一个枚举值可能与未初始化状态相同,从而造成假通过)。
  • 可操作的失败信息:优先使用精确匹配器(如 containsEntry、带错误文本的 isOk),而不是 assertTrue(result.ok())。独立检查应继续执行(EXPECT),除非后续断言已无意义(文件未打开时使用 ASSERT)。
  • 浮点数:使用 容差,绝不要求精确相等。
  • UI 定位器:使用稳定的测试 ID,而不是文案或脆弱的 XPath。

5. 保持隔离与确定性

单元测试中不使用真实时钟,不用 sleep 同步,不访问实时网络/磁盘,也不假定共享文件/数据行为空。

  • 注入时钟,并让它 前进。
  • 使用带超时的闩锁/事件等待,或推进伪调度器。
  • 每个测试使用唯一的临时路径/隔离的测试夹具。
  • 通过测试替身触发罕见故障(超时、抛出错误)——不能依赖实时基础设施。

6. 证明测试能够失败

在本地编写或编辑时:先运行并确认通过,再破坏生产代码(或反转断言),确认测试 失败。审查时若无法运行:写出这条断言能捕获的一个具体行为错误;如果写不出来,就把它视为变更检测器,不要批准。

重构测试时,先让生产代码处于已知错误状态,以便发现被删除的断言;然后恢复生产代码。未经这项检查,绝不进行测试的“绿到绿”清理。

如果剩下的唯一测试是同义反复、实现的模拟脚本,或没有逻辑的连线验证:不要添加它。应指出缺失的接缝,或说明应由哪一层承担该风险。

如果项目约定与此处的硬性规则冲突(要求模拟所有协作者、基于 sleep 的等待、只测试私有辅助方法):停止并指出冲突。不要默默遵循较弱的约定。

审查清单

愿意合并的测试:

  • 会因真实的行为错误而失败,而不只是因重命名/提取而失败
  • 断言结果/状态,而不是调用脚本(除非调用本身就是契约)
  • 覆盖公开 API / 面向用户的路径,而不是无法到达的辅助方法
  • 原因与结果清晰可见;名称说明了预期结果
  • 具有确定性(时钟/IO/唯一性均已处理)
  • 失败信息能直接指导修复,无需额外日志

评论

支持 Markdown