principle-test-behavior-not-implementation
测行为,不测实现
Section titled “测行为,不测实现”测试是在声明代码为使用者做了什么。使用者可以是调用方、终端用户或另一个服务。行为是使用者通过自己使用的接口能观察到的东西,包括返回值、错误、能读回的状态,以及代码对外部系统造成的影响,比如发出请求、写入一行记录或发出消息。实现是使用者观察不到的东西,比如私有辅助函数、内部调用顺序、中间数据,以及具体由哪个内部协作者完成工作。
用两种假想的修改检验每个测试:
- 破坏行为。 让代码返回错误值、跳过某个效果,或把同一个效果执行两次。如果测试仍能通过,它什么也证明不了。常见原因有:断言比契约宽松,期望值由被测代码算出,复述常量或提示词字符串却没有运行读取它的代码,以及断言测试自己构造的数据。
- 修改内部实现。 保持所有可观察结果不变,只重构底层代码。如果测试因此失败,说明它绑定了实现细节。每次重构都得重写,却仍然抓不到 bug。
保留能抓住第一种修改、又能在第二种修改后通过的测试。其余的重写或删除。要写出这样的测试,就用具体输入,通过使用者的接口驱动代码。期望值要从需求推导,不能来自被测代码的一次运行。断言契约承诺的内容,不多也不少。
该测什么由契约决定,不由断言的形式决定。当契约承诺「空购物车永远不扣款」时,断言支付客户端没有收到调用就是在测行为。再配一个会扣款的用例,证明测试确实能发现调用。mock 的外部服务收到的载荷也是行为,因为该服务能观察到它。调用内部辅助函数则属于实现,因为使用者看不到。没有匹配记录时,空列表是确切答案;如果测试只需要得到某个结果,空列表就只是空洞的检查。断言精确的错误消息,会绑死调用方并不阅读的措辞。调用方根据它选择分支的错误类型,才是契约。
测试也可以锁定不变量,而不是调用,比如必须同时存在于两张表中的同一个键,或编译期类型检查。不变量本身就是行为。
