test-audit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test audit

测试审计

A test earns its maintenance cost by catching a break someone would call a bug. One bar applies both when writing tests and when deciding which existing ones to delete. For pruning a whole subsystem's tests in one change, read reference/campaign.md first.
一个测试只有在能捕获某个人会称为 bug 的故障时,才值得付出相应的维护成本。 编写测试和决定删除哪些现有测试时,适用同一套标准。若要在一次变更中清理整个子系统的测试,请先阅读 reference/campaign.md。

Before writing a test

编写测试之前

Answer all four; a missing answer means don't add the test yet.
  1. What production change makes it fail, and is that change a bug or a decision? If only an intentional decision can fail it (a constant's value, exact wording, private structure), it is a change detector: it fires on redesign and sleeps through bugs. Test the behavior that depends on the decision instead: not
    MAX_RETRIES == 5
    , but "the sixth attempt never happens".
  2. Where is the expected value from? A literal, a worked example, or the spec. Never the code under test or its helpers:
    expect(f(x)).toBe(f(x))
    passes by construction. An expectation that re-derives the value with the implementation's own formula shares that formula's bugs.
  3. Why doesn't existing coverage catch it already? Each contract has one owning test at its strongest boundary, usually the public interface. A second layer needs a risk the owner can't reach, such as a transport or lifecycle failure. Prefer a new row in an existing table test over a near-duplicate test.
  4. Does it need a production seam that no production caller needs? An export, flag, getter, reset hook or
    destroy()
    added for the test. If so, test at the real boundary instead; cleanup only tests need goes in test utilities.
Then check it against the junk patterns. A match fails the gate unless the retention bar names the contract it independently guards. A test that breaks under a behavior-preserving refactor is asserting implementation; rewrite it at the owning boundary before landing it.
Don't test the framework (asserting your router calls the handler you registered), and don't test trivial code: constructors, getters and plain forwarding earn a test only when they validate, normalize, default, derive or cause a side effect.
请回答全部四个问题;只要有一个问题没有答案,就暂时不要添加该测试。
  1. 什么生产代码变更会使它失败,而且该变更是 bug 还是一项决策? 如果只有有意作出的决策才能使它失败(常量的值、确切措辞、私有结构),那它就是一个变更检测器:重构时会被触发,却对 bug 视而不见。应改为测试依赖该决策的行为:不要断言
    MAX_RETRIES == 5
    ,而应验证“第六次尝试绝不会发生”。
  2. 预期值从哪里来? 来自字面值、完整推导的示例或规范。绝不能来自被测代码或其辅助函数:
    expect(f(x)).toBe(f(x))
    会按构造方式通过。使用被测实现自身的公式重新推导预期值的断言,也会继承该公式的 bug。
  3. 为什么现有覆盖还不能捕获它? 每项契约都应有一个归属测试,并在该契约最强的边界上进行验证,通常是公共接口。第二层测试需要覆盖归属测试无法触达的风险,例如传输或生命周期故障。优先在现有表格测试中新增一行,而不是添加近似重复的测试。
  4. 它是否需要任何生产调用方都不需要的 production seam? 例如为了测试而添加的 export、flag、getter、reset hook 或
    destroy()
    。如果是这样,请在真实边界上进行测试;仅测试所需的清理逻辑应放入测试工具中。
然后根据垃圾模式检查该测试。如果匹配到其中任一模式,除非保留标准明确指出它所独立守护的契约,否则测试未通过准入门槛。一个在保持行为不变的重构下就会失败的测试,说明它断言的是实现细节;在合入前,应在归属边界上重写该测试。
不要测试框架本身(例如断言 router 调用了你注册的 handler),也不 要测试琐碎代码:constructor、getter 和纯转发逻辑只有在负责验证、规范化、提供默认值、派生结果或产生副作用时才值得测试。

Regression tests

回归测试

A regression test must fail on the pre-fix code, for the reason the bug describes, and pass after the fix. Run it against the old code to see it. An error the bug doesn't cause (import failure, missing selector, broken setup) proves nothing; fix it until the test fails the way the bug does. A regression test that never failed proves the mock, not the fix. Write one, at the owning boundary; don't replay the same scenario at every layer it crosses.
回归测试必须能够在修复前的代码上失败,且失败原因必须是 bug 所描述的原因,并在修复后通过。请针对旧代码运行它来确认这一点。不是该 bug 引起的错误(导入失败、缺少选择器、setup 损坏)不能证明任何问题;应修复测试,直到它以 bug 所对应的方式失败。一个从未失败过的回归测试证明的是 mock,而不是修复。只在归属边界上编写一个回归测试,不要在它经过的每一层重复播放同一个场景。

Mocks

Mocks

  • Never assert on the mock. An assertion that passes when the mock exists and fails when it's removed says nothing about your code.
  • Before mocking a method, list its side effects. Mock the slow or external level below the ones the test depends on, not the method itself.
  • Mock responses mirror the complete real structure, not just the fields this test reads; a partial mock passes while integration breaks.
  • When arguments, call counts or ordering are the contract, assert them. Give each branch (success, error, malformed) its own fixture, so the wrong branch can't satisfy the test.
  • When mock setup outgrows the test, or you can't say why the mock is needed, use real components with a fake network or filesystem instead.
  • 绝不要断言 mock 本身。一个在 mock 存在时通过、删除 mock 后便失败的断言,无法说明你的代码有任何问题。
  • 在 mock 某个方法之前,先列出其副作用。应 mock 测试所依赖边界之下的缓慢或外部层级,而不是 mock 该方法本身。
  • Mock 响应应镜像完整的真实结构,而不只是当前测试读取的字段;部分 mock 会在集成环节已经失效时依然通过。
  • 当参数、调用次数或顺序本身构成契约时,应对它们断言。为每个分支(成功、错误、格式错误)设置各自的 fixture,使错误分支无法满足测试。
  • 当 mock setup 的复杂度超过测试本身,或你无法说明为何需要 mock 时,改用真实组件配合 fake network 或 filesystem。

Before finishing

结束之前

Mentally mutate the production code: wrong constant or argument, wrong branch, missing side effect, empty return, missing check for empty, zero, null, unauthorized or malformed input. At least one test should fail for each. A mutation nothing catches marks an unprotected behavior or a tautological test.
在头脑中对生产代码进行变异:使用错误的常量或参数、进入错误分支、遗漏副作用、返回空值,或遗漏对 empty、zero、null、unauthorized 或 malformed 输入的检查。每种变异都至少应使一个测试失败。没有任何测试能够捕获的变异,意味着存在未受保护的行为或同义反复的测试。

Auditing existing tests

审计现有测试

  1. Read the root and scoped
    AGENTS.md
    /
    CLAUDE.md
    , then, for each candidate: the complete test, the production code it covers, its callers, overlapping tests, CI routing and
    git log
    for why it exists. If it claims dependency-backed behavior, read the dependency's source or types.
  2. Keep discovery read-only. Prefer a few well-evidenced candidates over a long speculative list. For a broad scope, run parallel read-only agents split by area.
  3. Record the evidence for each candidate before editing. A missing field means it isn't ready to delete.
  4. Edit one coherent batch per change. Delete the test-only exports, flags, wrappers and dead production paths the deletion unlocks, rather than keeping aliases. Move retained regressions to their owning suite. Don't add replacement tests that restate the same implementation.
  5. Run the smallest owning and sibling tests, then the gate the repo requires for changed files. For a deleted source-text assertion, run the script or dry-run that owns the real contract.
  6. Report production and test line counts separately (
    git diff --numstat
    ), plus the tests you kept that looked like junk, and why.
  1. 阅读根目录和作用域内的
    AGENTS.md
    /
    CLAUDE.md
    ,然后针对每个候选项阅读:完整测试、它覆盖的生产代码、调用方、重叠测试、CI 路由,以及用于了解其存在原因的
    git log
    。如果它声称验证依赖项支持的行为,请阅读该依赖项的源代码或类型定义。
  2. 保持发现过程只读。相比冗长的推测清单,宁可选出少量证据充分的候选项。对于广泛范围,运行按领域拆分的并行只读 Agent。
  3. 在编辑前记录每个候选项的证据。缺少任何字段,都表示它尚未准备好被删除。
  4. 每次变更编辑一个内聚的批次。删除测试专用的 export、flag、wrapper,以及因删除测试而不再需要的 dead production path,而不是保留别名。将保留的回归测试迁移到其归属测试套件。不要添加只是重述相同实现的替代测试。
  5. 先运行最小范围的归属测试和同级测试,然后运行仓库针对变更文件要求的 gate。如果删除的是源代码文本断言,请运行负责验证真实契约的脚本或 dry-run。
  6. 分别报告生产代码和测试的代码行数(
    git diff --numstat
    ),并说明哪些看似无用的测试被保留,以及原因。

Gotchas

容易忽略的问题

  • Static or slow is not a reason to delete. A test that reads source text can be the cheapest independent guard of a user-facing key, byte or path. Keep it if it fails when that contract changes and survives renaming an identifier. Keep the others the retention bar lists.
  • A deletion candidate that fails on the base branch is a possible product bug. Reproduce it and fix the production code; don't delete the test that found it.
  • Judge a test by its assertions, not its name. A test named "retires the window" once asserted that the window was not cleared.
  • Check negative tests for the right reason. A denial from a different guard, or a rejection the production path never reaches, passes while protecting nothing.
  • Don't delete for the count. Uncertain candidates stay. Optimize for confidence in what remains.
  • Don't change tests while the suite is running in that checkout. Watch mode and parallel runners pick up half-edited files and report failures that don't exist.
  • 静态或运行缓慢不是删除理由。 读取源代码文本的测试,可能是保护面向用户的键、字节或路径的最廉价独立防线。如果它在契约变化时会失败,并且重命名标识符后仍能存续,就应保留。对于保留标准列出的其他测试,也应保留。
  • 如果删除候选项在基础分支上失败,它可能揭示了产品 bug。 复现问题并修复生产代码;不要删除发现该问题的测试。
  • 根据断言判断测试,而不是根据名称。 一个名为“retires the window”的测试曾经断言窗口不会被清除。
  • 确认负向测试因正确的理由而拒绝。 如果拒绝来自另一道 guard,或者生产路径根本不会触发该拒绝,测试即使通过也没有保护任何东西。
  • 不要为了数量而删除。 不确定的候选项应保留。应以对保留内容的信心为优化目标。
  • 不要在该 checkout 的测试套件运行时修改测试。 Watch mode 和并行 runner 可能读取到编辑到一半的文件,并报告实际并不存在的失败。