principle-test-behavior-not-implementation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTest Behavior, Not Implementation
测试行为,而非实现
A test is a claim about what the code does for its users: a caller, an end user, or another service. Behavior is what those users can observe through the interface they use. That includes return values, errors, state they can read back, and effects on systems outside the code, such as a request sent, a row written, or a message emitted. Implementation is what they cannot observe: private helpers, internal call order, intermediate data, and which internal collaborator did the work.
Judge each test against two imagined edits:
- Break the behavior. Make the code return a wrong value, skip an effect, or do the effect twice. If the test still passes, it proves nothing. The usual causes are an assertion looser than the contract, an expected value computed by the code under test, a restated constant or prompt string that never runs the code that reads it, and an assertion on data that the test built itself.
- Change the internals. Keep every observable result the same and restructure the code underneath. If the test now fails, it is pinned to the implementation. It will need a rewrite on every refactor and still catches no bug.
Keep a test that catches the first edit and survives the second. Rewrite or delete the rest. To write a test that does both, drive the code through its users' interface with a concrete input. Derive the expected value from the requirement, not from a run of the code. Assert what the contract promises, no less and no more.
The contract decides, not the shape of the assertion. When "an empty cart never charges" is the promise, an assertion that the payment client got no call is behavior. Pair it with a case where the charge happens, so that the test shows it can see a call. The payload that a mocked external service receives is behavior, because that service observes it. A call to an internal helper is implementation, because no user can see it. An empty list is the exact answer when no record matches, and a hollow check when the test only needs some result. An exact error message pins wording that no caller reads. The error type that callers branch on is the contract.
A test can pin an invariant instead of a call, such as a key that must exist in two tables or a compile-time type check. The invariant is the behavior.
测试就是一项主张:代码会为其用户做什么。这里的用户可以是调用方、终端用户或其他服务。行为是这些用户能够通过所使用的接口观察到的内容,包括返回值、错误、可以重新读取的状态,以及对代码外部系统产生的影响,例如发送请求、写入数据行或发出消息。实现则是用户无法观察到的内容,包括私有辅助函数、内部调用顺序、中间数据,以及究竟由哪个内部协作者完成了工作。
用两次假想修改来评判每个测试:
- 破坏行为。 让代码返回错误的值、跳过一次效果,或者将同一次效果执行两次。如果测试仍然通过,就说明它没有证明任何东西。常见原因包括:断言比契约更宽松、预期值由被测代码计算得出、只是重复声明了一个从未触发读取代码的常量或提示词字符串,以及断言了测试自行构造的数据。
- 更改内部实现。 保持所有可观察结果不变,同时重构底层代码。如果此时测试失败,就说明它绑定到了实现细节。测试将需要在每次重构时重写,而且仍然无法发现任何缺陷。
保留能够发现第一种修改、且不受第二种修改影响的测试。重写或删除其余测试。要编写同时满足这两个条件的测试,请使用具体输入,通过用户的接口驱动代码。预期值应源于需求,而不是根据代码的一次运行结果得出。断言契约所承诺的内容,既不少也不少。
起决定作用的是契约,而不是断言的形式。当契约承诺“空购物车绝不扣款”时,断言支付客户端没有收到调用属于行为测试。应同时搭配一个确实发生扣款的测试用例,以证明该测试能够观察到这个调用。模拟的外部服务所接收到的 payload 属于行为,因为该服务能够观察到这个 payload。调用内部辅助函数则属于实现细节,因为任何用户都看不到它。当没有匹配记录时返回空列表是精确答案;如果测试只需要某个结果,那么这种检查就是空泛的检查。精确匹配错误消息会绑定没有任何调用方会读取的措辞,而调用方用于分支判断的错误类型才是契约。
测试也可以固定某个不变量,而不是固定一次调用,例如某个键必须同时存在于两个表中,或者某项编译时类型检查。不变量就是行为。