writing-unit-tests
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWriting unit tests
编写单元测试
REQUIRED BACKGROUND: the skill. Which tests a change owes is ; this skill is the craft of the tests themselves.
principal-engineeringtesting-changes必备背景知识: skill。变更需遵循的测试规则见;本内容聚焦测试本身的编写技巧。
principal-engineeringtesting-changesOverview
概述
A unit test is a behavioral claim with a name, read by the next engineer during a red build. Core principle: test the contract, not the implementation; make the name carry the claim; and keep the test so simple it cannot itself be wrong.
单元测试是带有名称的行为断言,供后续工程师在构建失败时查阅。核心原则:测试契约而非实现细节;让测试名称明确表达测试主张;保持测试足够简单,确保测试本身不会出错。
Behavior, not implementation
测试行为,而非实现
- Test through the public contract of the unit. A refactor that preserves behavior should not break tests; when it does, the tests were asserting the implementation, and they now punish improvement.
- Do not assert call sequences, internal state, or that method A called method B, unless the interaction IS the contract (a required side effect on a boundary). Asserting internals tests the implementation twice and the behavior zero times.
- Never derive the expected value from the production arithmetic, neither by reimplementing the formula nor by invoking the shared helper that computes it. Both prove the code equals itself, and both stay green when the shared code carries the bug. Expected values are literals worked out independently (by hand, from a spec, from real data), with the derivation in a comment.
- 通过单元的公共契约进行测试。保留行为的重构不应破坏测试;若重构导致测试失败,说明测试在断言实现细节,这类测试会阻碍代码优化。
- 除非交互本身就是契约(边界上的必要副作用),否则不要断言调用顺序、内部状态或方法A是否调用了方法B。断言内部细节相当于重复测试实现,却完全未测试行为。
- 绝不要通过重实现公式或调用生产代码的共享工具来推导预期值。这两种方式只能证明代码等于自身,当共享代码存在bug时,测试仍会显示通过。预期值应是独立计算的字面量(手动计算、依据规范或真实数据),推导过程需写在注释中。
One behavior per test, named as the claim
每个测试对应一种行为,名称明确表达断言
- One behavior per test; splitting is cheaper than archaeology on a multi-assert failure.
- The name states subject, scenario, and expected outcome: , not
expired_token_is_rejected_with_401. Test names describe behavior, state transitions, and invariants; never delivery order, ticket keys, or phases. Read the test list of a module and you have read its spec; that is the bar.test_auth_3 - Arrange, act, assert, visibly and in that order. No branching, loops, or logic in a test: a test with logic needs its own test. Shared setup earns a builder or a role-named fixture; a mystery blob fixture hides which arranged fact the assertion depends on. Generation and iteration live in builders and helpers, not in the test body; property-based tests are the accepted form for invariants and follow their framework's shape, while example-based tests stay logic-free.
- 每个测试对应一种行为;拆分测试的成本远低于在多断言失败时排查问题的成本。
- 测试名称需包含测试主体、场景和预期结果:例如,而非
expired_token_is_rejected_with_401。测试名称应描述行为、状态转换和不变量;绝不能包含交付顺序、工单编号或阶段。阅读模块的测试列表,就能了解该模块的规格,这是标准要求。test_auth_3 - 按准备(Arrange)、执行(Act)、断言(Assert)的顺序清晰编写测试。测试中不得包含分支、循环或逻辑:带有逻辑的测试本身也需要被测试。共享的准备逻辑应封装为构建器或具名测试夹具;模糊的测试夹具会隐藏断言依赖的前置条件。生成和迭代逻辑应放在构建器和工具类中,而非测试主体内;属性测试是验证不变量的标准形式,需遵循其框架格式,而示例测试需保持无逻辑。
Determinism
可预测性
- No real time, real randomness, real network, or real filesystem inside a unit test: inject the clock, seed or inject the randomness, fake the boundary. The test that passes at 14:00 and fails at midnight is a bug report about the test.
- No sleeps. Waiting for async work is condition-based (poll the observable outcome with a deadline), never duration-based; a sleep is a race condition with a timer attached.
- A flaky test is red: fix it or quarantine it visibly with an owner (see the red-test rule in ); re-running until green is silencing a detector.
testing-changes
- 单元测试中不得使用真实时间、真实随机数、真实网络或真实文件系统:应注入时钟、种子或随机数模拟、边界模拟。那种14:00通过、午夜失败的测试本身就是一个bug。
- 不得使用sleep。等待异步任务应基于条件(在截止时间内轮询可观测结果),而非固定时长;sleep本质是带有计时器的竞态条件。
- 不稳定测试(flaky test)等同于失败测试:修复它或明确标记隔离并指定负责人(参见中的red-test规则);反复执行直到通过相当于屏蔽了问题检测器。
testing-changes
Mocks are assumptions
Mock是假设
- Mock the boundaries you do not own (network, clock, filesystem, third-party services); prefer real collaborators for code you do own within the unit's reach. For owned wrappers around unowned resources (your repository class fronting the database), fake at the seam where owned code last touches the unowned resource, and keep the test data role-named and visible either way. Every mock hardcodes an assumption about a contract, and a stale mock is how a suite stays green while the real integration is broken.
- When a test is mostly mock wiring, it is testing the mocks. Either widen the unit to something with real behavior or accept that this seam needs an integration test instead (and say which).
- Fixtures are labeled snapshots of reality: minimal, role-named for their part in the scenario, and updated deliberately when the contract changes, never regenerated blindly to make red go green.
- 仅对非自有边界(网络、时钟、文件系统、第三方服务)使用Mock;对于单元范围内的自有代码,优先使用真实协作对象。对于封装非自有资源的自有代码(如封装数据库的仓储类),应在自有代码与非自有资源的交接处进行模拟,同时确保测试数据具名且可见。每个Mock都硬编码了对契约的假设,过时的Mock会导致测试套件显示通过,但实际集成已失效。
- 如果测试大部分内容是Mock配置,那它其实是在测试Mock本身。要么扩大测试单元范围以覆盖真实行为,要么承认该边界需要集成测试(并明确说明)。
- 测试夹具是真实场景的最小化快照:需根据其在场景中的作用命名,当契约变更时需主动更新,绝不能为了让测试通过而盲目重新生成。
Assertions and failure paths
断言与失败路径
- Assert outcomes with values, not absence of exceptions. "It did not throw" claims almost nothing.
- Failure paths are first-class test subjects: the typed failure surfaces, the degraded mode is entered loudly, the guard actually guards (see ). The error path without a test is the silent swallow's favorite hiding place.
handling-failures - Tests themselves follow the no-silent-swallows contract: no catch-and-ignore in test code, no conditional assertions that skip silently when a precondition is absent; a test that cannot run must fail or be visibly skipped with the reason.
- 用具体值断言结果,而非断言未抛出异常。“未抛出异常”几乎无法证明任何内容。
- 失败路径是一等测试对象:需验证类型化错误是否暴露、降级模式是否被触发、保护机制是否生效(参见)。没有测试覆盖的错误路径是静默吞错的高发区。
handling-failures - 测试本身需遵循“无静默吞错”原则:测试代码中不得捕获异常后忽略,不得在前置条件缺失时跳过断言却不提示;无法运行的测试必须失败或明确标记跳过并说明原因。
Common mistakes
常见错误
- The mirror test: reimplementing the production logic to compute the expectation.
- The mock echo chamber: stubbing your own class and asserting the stub.
- The mega-test: twelve assertions, one name, no way to know which claim broke.
- Shared mutable fixtures that make test order matter; every test builds or receives its own state.
- The sleep that "fixes" flakiness by making it rarer.
- Green-checking the fixture: editing expected values to match actual output without deriving why the new value is right.
- 镜像测试:重实现生产逻辑来计算预期值。
- Mock回音室:模拟自有类并断言模拟对象。
- 巨型测试:包含12个断言,只有一个名称,无法定位哪个断言失败。
- 共享可变夹具导致测试顺序影响结果;每个测试应构建或获取独立的状态。
- 用sleep“修复”不稳定测试,实则只是降低了失败概率。
- 盲目修改预期值:编辑预期值以匹配实际输出,却不推导新值的合理性。