testing-on-the-toilet
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting on the Toilet
Testing on the Toilet(厕所测试指南)
Distilled from Google's Testing on the Toilet episodes (2007–2026). Episode index: .
references/episodes.mdRun this loop on every new test and every test you review. Completion: each test that ships would fail on a real behavior bug and would survive a behavior-preserving refactor.
本文提炼自Google的Testing on the Toilet系列文章(2007–2026)。文章索引见:。
references/episodes.md在编写每一个新测试以及评审每一个测试时,都遵循以下流程。最终目标是:每一个上线的测试都能在出现真实行为bug时失败,且能在不改变行为的重构中存活下来。
1. Change-detector gate
1. 变更检测器(Change-detector)检查
A change-detector restates production structure (especially ordered of every collaborator call) without checking an observable result. Correct and incorrect implementations pass equally; any rename/extract/reorder fails the suite.
verifyRewrite it to assert state, return value, rendered output, or persisted data. If you cannot name the user-visible contract, delete the test and say why. Do not "fix" a refactor by mechanically updating fifty mirrors.
Interaction is allowed only when the call itself is the contract (send mail once, do not double-charge, presenter tells the view to show X). If unsure, rewrite to a state/return assertion; if that is impossible, stop and ask which observable outcome the test protects.
verify**Change-detector(变更检测器)**会重复生产代码的结构(尤其是对每个协作方调用的有序),但不检查可观测的结果。正确和错误的实现都能通过测试,而任何重命名/提取/重排操作都会导致测试套件失败。
verify将其重写为断言状态、返回值、渲染输出或持久化数据。如果你无法明确用户可见的契约,删除该测试并说明原因。不要通过机械地更新五十个镜像来“修复”重构问题。
仅当调用本身就是契约时(比如发送一次邮件、不重复收费、通知视图显示X),才允许使用交互。如果不确定,先重写为状态/返回值断言;如果无法做到,停下来询问该测试要保护的可观测结果是什么。
verify2. Pick the cheapest layer that still has fidelity
2. 选择兼具保真度且成本最低的测试层级
Name the bug/risk first. Pick the topmost row in the table that can catch it. Escalate a row only when a cheaper layer would miss that risk (state the miss). SMURF (Speed, Maintainability, Utilization/cost, Reliability, Fidelity) is the tradeoff language when two rows could both catch it: prefer the cheaper/faster/more reliable one.
| Risk | Layer |
|---|---|
| Pure logic, parsing, validation, reducers | Fast unit through the public API |
| Collaborators you own, in-process | Real objects or an owner-maintained fake |
| Service/HTTP contract | Owner fake or hermetic server — not a handwritten request mock |
| UI wiring (disabled, unbound, hidden) | Drive the rendered control (click/type), not the handler |
| Cross-system critical path | Tiny e2e set: one path per use case plus key error classes; assert system outcomes, not copy/layout |
Brainstorm key risks before stacking layers. Coverage meters find gaps; they do not certify quality. Cover both sides of a branch (the implicit counts). Skip exhaustive combinatorics; extract predicates and cover each independently.
elsePrefer testing through the public API. Exhaustive tests of private helpers for inputs callers never pass are change-detectors in disguise.
先明确bug/风险类型。选择表格中能够发现该风险的最顶层层级。仅当更低成本的层级无法覆盖该风险时,才升级到更高层级(需说明遗漏的风险)。当两个层级都能覆盖风险时,用SMURF(速度、可维护性、利用率/成本、可靠性、保真度)作为权衡标准:优先选择成本更低、速度更快、可靠性更高的层级。
| 风险 | 层级 |
|---|---|
| 纯逻辑、解析、验证、状态归约器 | 通过公共API的快速单元测试 |
| 你拥有的进程内协作方 | 真实对象或由所有者维护的fake |
| 服务/HTTP契约 | 所有者提供的fake或封闭服务器 — 不要手写请求mock |
| UI连线(禁用、未绑定、隐藏) | 驱动渲染后的控件(点击/输入),而非处理函数 |
| 跨系统关键路径 | 小型端到端测试集:每个用例一条路径加上关键错误类型;断言系统结果,而非文案/布局 |
在堆叠测试层级前,先梳理关键风险。覆盖率工具能发现测试缺口,但无法保证测试质量。要覆盖分支的两端(隐式分支也需覆盖)。无需穷尽所有组合情况;提取断言条件并独立覆盖每个条件。
else优先通过公共API进行测试。针对调用方永远不会传入的输入,对私有辅助函数进行 exhaustive 测试,本质上就是伪装的变更检测器。
3. Choose the double
3. 选择测试替身(Double)
Order: real → fake → stub → mock. Mock last.
- Real when it is fast, deterministic, in-process.
- Fake: simplified working impl of a type you (or the library owner) maintain. Keep it narrow. Contract-test fake vs real when the fake is shared.
- Stub: canned returns for queries. Do not getters.
verify - Mock: verify side-effecting calls whose interaction is the spec.
Hard rules:
- Do not mock types you don't own. Wrap the third-party API; mock the wrapper; test the wrapper against the real library.
- Mocking more than one or two collaborators, or a long /
whenchain, is a seam smell — extract a narrower port and fake that.verify - Inject long-lived collaborators in the constructor; pass per-call work as method arguments. No hidden singletons, unmockable statics, or inside the logic under test.
now()
优先级:真实对象 → fake → stub → mock。Mock放在最后。
- 真实对象:当它速度快、确定性强且在进程内时使用。
- Fake:你(或库所有者)维护的简化版可用实现。保持功能范围狭窄。当fake被共享时,需对fake与真实对象进行契约测试。
- Stub:为查询请求提供预设返回值。不要对getter进行。
verify - Mock:验证那些交互本身就是规范的副作用调用。
硬性规则:
- 不要mock你不拥有的类型。封装第三方API;mock封装后的接口;针对真实库测试封装层。
- Mock超过一到两个协作方,或者存在冗长的/
when链,这是接口冗余的信号 — 提取更窄的端口并使用fake替代。verify - 在构造函数中注入长期存在的协作方;将单次调用的工作作为方法参数传入。禁止使用隐藏单例、无法mock的静态对象,或在测试逻辑内部调用。
now()
4. Author the test (DAMP)
4. 编写测试(遵循DAMP原则)
DAMP (descriptive and meaningful phrases) beats DRY in tests. Tests have no tests — a reader must see cause next to effect in the method body.
- One behavior per test. Name (or the project's equivalent).
unit_scenario_expectedOutcomeis not a name.testFoo - Arrange–act–assert in one block. Shared soup that lives far from the assertion is how wrong expected values sneak in.
@Before - Helpers hide irrelevant construction. Scenario-relevant fields stay visible in the test. Builder/factory with defaults is good; silently asserting a helper default is not.
- Literal inputs and expected outputs. Do not compute the expectation with loops/conditionals that can share a bug with production. If a helper must contain logic, unit-test the helper.
- Narrow assertions: the fields under test, not the whole object/screenshot/hash order. One full-equality check for the common happy object is enough.
- Distinct non-default values per input (,
0, first enum can match uninitialized state and fake a pass)."" - Actionable failures: precise matchers (,
containsEntrywith error text) overisOk. Independent checks should continue (assertTrue(result.ok())) unless later asserts are meaningless (EXPECTfile opened).ASSERT - Floats: tolerance, never exact equality.
- UI locators: stable test IDs, not copy or brittle XPath.
测试中**DAMP(描述性且有意义的表述)**优于DRY(不要重复自己)。测试本身没有测试 — 读者必须在方法体中直接看到因果关系。
- 每个测试对应一种行为。命名格式为(或项目约定的格式)。
unit_scenario_expectedOutcome不是合格的命名。testFoo - 安排–执行–断言放在一个代码块中。远离断言的共享代码块,会导致错误的预期值悄然混入。
@Before - 辅助工具用于隐藏无关的构造代码。与场景相关的字段要可见于测试中。带默认值的构建器/工厂是可行的;但不要静默断言辅助工具的默认值。
- 使用字面量输入和预期输出。不要用循环/条件计算预期值,否则可能与生产代码共享相同的bug。如果辅助工具必须包含逻辑,要对辅助工具进行单元测试。
- 窄断言:仅断言测试相关字段,而非整个对象/截图/哈希顺序。针对常见正常对象进行一次全相等检查即可。
- 每个输入使用不同的非默认值(、
0、第一个枚举值可能与未初始化状态匹配,导致虚假通过)。"" - 可操作的失败信息:使用精确匹配器(如、附带错误文本的
containsEntry)而非isOk。除非后续断言毫无意义(如assertTrue(result.ok())文件已打开),否则独立检查应继续执行(ASSERT)。EXPECT - 浮点数:使用容差,永远不要精确相等。
- UI定位器:使用稳定的测试ID,而非文案或脆弱的XPath。
5. Hermetic and deterministic
5. 封闭性与确定性
No real clock, no as synchronization, no live network/disk in unit tests, no shared files/rows assumed empty.
sleep- Inject a clock; tick it.
- Wait on latches/events with a timeout, or advance a fake scheduler.
- Unique temp paths / isolated fixtures per test.
- Force rare failures (timeouts, thrown errors) through the double — live infra cannot.
单元测试中不要使用真实时钟、不要用做同步、不要使用实时网络/磁盘、不要假设共享文件/数据行是空的。
sleep- 注入时钟;手动推进时钟时间。
- 通过超时等待锁存器/事件,或推进fake调度器。
- 每个测试使用唯一的临时路径/隔离夹具。
- 通过测试替身触发罕见故障(超时、抛出错误) — 真实基础设施无法做到这一点。
6. Prove the test can fail
6. 验证测试能够失败
When writing or editing locally: run green, then break production (or invert the assertion) and confirm red. When reviewing and you cannot run: write the one concrete behavior bug this assertion would catch; if you cannot, treat it as a change-detector and do not approve.
When refactoring tests, put production in a known-broken state first so dropped assertions show up; then restore production. Never green-to-green "cleanup" of tests without that check.
If the only remaining test is a tautology, a mock-script of the implementation, or wiring with no logic: do not add it. Report the missing seam or the layer that should own the risk instead.
If project conventions conflict with a hard rule here (required mock-all-collaborators, sleep-based waits, tests of private helpers only): stop and cite the conflict. Do not silently follow the weaker convention.
在本地编写或编辑测试时:先运行测试确保通过,然后破坏生产代码(或反转断言)并确认测试失败。在评审时如果无法运行测试:写出该断言能捕获的具体行为bug;如果无法写出,将其视为变更检测器,不予批准。
重构测试时,先将生产代码置于已知的故障状态,这样遗漏的断言会暴露出来;然后恢复生产代码。永远不要在不进行此检查的情况下,直接从“通过”到“通过”地“清理”测试。
如果剩下的唯一测试是同义反复、实现的mock脚本,或没有逻辑的连线测试:不要添加它。转而报告缺失的接口或应承担该风险的测试层级。
如果项目约定与本文的硬性规则冲突(如要求mock所有协作方、基于sleep的等待、仅测试私有辅助函数):停下来并指出冲突。不要默默遵循较弱的约定。
Review checklist
评审检查清单
A test you would merge:
- Would fail on a real behavior bug, not only on a rename/extract
- Asserts a result/state, not a call script (unless the call is the contract)
- Public API / user-facing path, not an unreachable helper
- Cause and effect visible; name states the outcome
- Deterministic (clock/IO/uniqueness handled)
- Failure message would start the fix without extra logging
可合并的测试需满足:
- 会在真实行为bug出现时失败,而非仅在重命名/提取操作时失败
- 断言结果/状态,而非调用脚本(除非调用本身就是契约)
- 针对公共API/用户可见路径,而非不可达的辅助函数
- 因果关系可见;命名明确预期结果
- 具有确定性(时钟/IO/唯一性已处理)
- 失败信息无需额外日志即可指导修复",