每周技巧 #135:测试契约,而不是实现
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #135: Test the Contract, not the Implementation。
原文最初作为 TotW #135 发布于 2017 年 6 月 5 日。
更新于 2020 年 4 月 6 日。
快捷链接:abseil.io/tips/135
“如果你有一个真正的朋友,你拥有的就已经超过了你的份额。” —— Thomas Fuller
C++ 有一套相当复杂的访问控制机制,使用 public、protected、private 和 friend。测试代码在使用这些设施时有自己的礼仪规则,GoogleTest 还用自己的 FRIEND_TEST 宏增强了这些规则。使用 FRIEND_TEST 应该是最后手段,而不是优先选择。
测试契约
我们写测试,是为了发现组件契约实现中的 bug,或者让我们有足够信心相信没有这类 bug。使用测试驱动开发(TDD)时,我们也写测试来帮助设计这个契约。依赖组件未规定方面的测试很脆弱,即使生产代码工作正确,也容易报告失败。
优先通过组件的公开接口进行测试。更一般地说,测试应该验证组件契约,并且和任何其他客户端一样,不应做出超出保证范围的假设。
从测试提供访问的技术
有许多技术可以让测试代码获得完成工作所需的访问权限。下面列出一些,大致按从好到坏排序。
为测试增强公开 API
有时通过最小接口进行测试,很难获得足够覆盖率。如果你的组件实现的是基类指定的一个非常窄的接口(例如只有一个 ProcessItem 虚函数),而只使用该接口的测试无法实际提供足够信心,可以考虑创建一个新的、可测试的组件来包含实现细节。这样包含虚函数的类就可以非常简单,只需要最少测试。BUILD 可见性可以用于限制实现类的使用(如果需要,并且你的构建系统支持)。
如果某个测试只依赖一两个私有函数,可以考虑把这些函数变成公开接口的一部分。这并不太糟:无论如何你都需要给它们一个清楚记录的接口,而且其他客户端(不只是测试)也可能发现它们有用。如果经过考虑后,你认定某个函数确实只用于测试,那么应该这样记录它,并且也许用 ForTesting 后缀命名。
使用 peer 避免暴露实现
如果测试仍然需要访问私有实现细节,可以创建一个 test peer(有时也叫 test spouse)。test peer 是被测类的友元类,通常定义在 _test.cc 文件中(不过有些人更喜欢把它定义在授予友元关系的类所在文件中),用于向测试代码提供对被测类的受控访问。test peer 不能位于匿名命名空间中,因为它的精确名称需要匹配 friend 声明;但测试代码的其余部分可以像往常一样位于匿名命名空间中。test peer 类名通常以 Peer 结尾。
(避免)使用 FRIEND_TEST
虽然 FRIEND_TEST 在旧代码中很常见,但新代码不应该使用它。它会引入反向耦合,让生产头文件依赖相关单元测试的细节。它迫使测试移出匿名命名空间。每个 FRIEND_TEST 都授予一个测试函数对被测类的不受限访问;在很长的测试函数中,很难看出测试在哪里修改了被测类状态。它要求在生产代码中包含 GoogleTest 提供的一个不寻常头文件,而 GoogleTest 几乎全部内容本来都只用于测试。最后,它扩展性很差:新增测试时,需要向生产头文件添加新的 FRIEND_TEST 用法。实践中,这常常导致头文件里出现很长一段 FRIEND_TEST 行。
(不要)把整个测试夹具设为 friend
强烈不建议把整个测试夹具设为被测类的友元(使用 friend class MyClassTest;)。和上面的选项相比,它允许整个测试夹具(但不是测试本身,因为测试是夹具的子类)不受限制、也无标注地访问被测类的每个成员,这意味着测试代码读者没有视觉线索判断测试何时打破了封装。它也会迫使测试夹具位于匿名命名空间之外。与友元夹具相比,test peer 能让代码对读者更加自文档化,而作者只需多做一点点工作。
建议总结
- 优先测试组件的客户端接口,并让测试独立于私有实现细节。
- 如果客户端接口不足以充分演练被测单元,就拆出一个可测试的、可能只用于测试的子组件。
- 有时为了让组件可测试,向公开接口添加内容是合理的。
- 如有必要,从测试访问私有成员时,使用 test peer,而不是
FRIEND_TEST。 - 不要把整个测试夹具设为友元。使用上面描述的更有针对性的方式之一。