0%

测试接口而不是实现

这个类真的需要测试吗?

1
2
3
4
5
class UserInfoValidator {
public void validate(UserInfo info) {
if (info.getDateOfBirth().isInFuture()) { throw new ValidationException()); }
}
}

乍一看,这个类的方法包含了一些逻辑,所以可能最好测试一下。但是如果唯一的调用者如下所示,又该怎么讲呢?

1
2
3
4
5
6
7
public class UserInfoService {
private UserInfoValidator validator;
public void save(UserInfo info) {
validator.validate(info); // Throw an exception if the value is invalid.
writeToDatabase(info);
}
}

答案可能会变成:UserInfoValidator类可能不需要测试了,因为所有路径都可以通过UserInfoService被测试到。最重要的分别在于UserInfoValidator类是实现细节,而不是一个public API。

一个public API可以被多个用户使用,这些用户又可能将所有入参的任意组合传给TA的方法。你想要保证这些组合被完整的测到了,以防止使用者在使用这些API时碰到问题。public API的代表有:在代码库的另一处被使用的类,以及在整个代码库到处被使用的Utils类等。
一个实现细节类存在的价值就是来支撑public APIs,并且只被极少数调用者使用(大多数情况都只有一个)。这些类有时可以间接的做测试覆盖,如何做间接测试覆盖?通过测试调用它的public APIs。
测试实现细节类在许多场景下还是有其意义的,例如,有时这些实现细节类很复杂,亦或是很难通过public APIs测试到。当你确实要测试TA们时,TA们往往也不需要像public APIs一样的测试深度,因为有些输入永远也不会被传递给TA们(在上面的代码示例中,如果UserInfoService保证了UserInfo一定不为null,那么就没必要测试UserInfoValidator.validate在info为null时会如何表现了,由于这永远不会发生)。

实现细节类有时可以被看成私有方法,只不过恰巧作为一个独立的类存在而已,因为你一定不会想去直接测试私有方法。

你也应该尽力控制实现细节类的可见性,比如在Java中将其设置为包内私有。

过度测试实现细节的类容易导致如下问题:

  • 代码更难以维护,因为你需要频繁的修改测试用例, 比如当改变一个实现细节的类的方法签名,或是做一个简单的重构。如果测试是通过public APIs覆盖的,那么这些变更就完全不会影响到测试用例。
  • 如果只通过实现细节类来测试行为,你可能会对代码产生错误的自信,因为同样的代码路径在public APIs使用时可能并不work。并且在重构时也要花更多的心思,因为如果不是所有路径都通过public APIs来测试的,那么很难保证public API的行为在重构中不受影响。

可能的问题

Q1: 这是在反对测试金字塔理论,而鼓励更大规模的测试(集成测试,而不是单元测试)吗?
A1:个人认为本文与测试金字塔(更专注于小规模测试)并不冲突,因为用pubic APIs的方式依然可以测试代码的一小部分。只不过,用过public APIs来测多个类而不是只测一个类,理应不是什么问题。

Q2:一个public方法就是一个public API,与被多少调用者使用无关。整个关于实现细节类的讨论都没什么意义。一个public方法应该被测试,作者错误认知的源头在于,TA看了一眼源码,然后得出这样的结论:

  • [哦,这是实现细节类,我不用测TA了]
  • [哦,这个类只被另外一个方法使用了,我可以相应的调整我的测试了]
    一个待测类应该被作为一个黑盒,不要对其做任何假设,就把它当做潜在的toxic测试。永远不应该去看你要测试的类的源码。只有在你想要修改代码时,才需要去看具体实现。

A2: 关于一个类应该被作为黑盒测试,本人没有任何异议。但不是所有的类都应该被同等对待。只是因为开发者决定将一些logic放到独立的类里,并不能自然的指向这些逻辑就需要被深度测试了。
关于一个类要做什么程度的测试,不决定于TA如何被使用,也不决定于TA的实现:如果一个类只在一个地方使用,且其他地方访问不到TA,那么TA也不是public API。所以这一切与实现无关。

写在最后

上述Q2中的问题,实际上极具含金量,短短几句话道出了测试应该怎么做。博文作者对于该说法深表赞同。然而唯一的认知差距在于是否所有独立的类的public方法都需要深度测试,
作者认为,可以认为实现细节类在其契约上就清晰的表达,该类不应该被其他人使用。这样Q2就与本文没有冲突了。

总结一下测试的要点:

  • 把待测API/系统作为黑盒,只关心其公开的契约,不关心其实现
  • 过渡关心实现细节,会给后续维护带来较重负担
  • 测试用例的最佳评价标准是:只要API的语义没变,那么测试用例就可以不变

参考文献:

https://testing.googleblog.com/2015/01/testing-on-toilet-prefer-testing-public.html