单元测试有助于避免尴尬、耗时的错误,将测试作为安全网只是一部分,更大部分是将测试表达为代码的思考过程。
接下来的内容提炼自《单元测试的艺术(第2版)》和《有效的单元测试》两本书。
一、质疑和回答
在组内推广时,进度并不理想,遇到的阻碍大致可归纳为以下这几种情况:
- 首先就是团队成员会质疑单元测试的价值,需要给出证明单元测试确实有效可行的方法和证据。
- 其次是团队成员缺乏主动测试的意识,目前有大量的测试代码,不知道从哪里开始测试,并且花费额外的精力来维护单元测试的代码。
- 还有就是编写单元测试大概会花费日常开发的 10% 以上的时间,而项目时间总是比较紧,无法留出充裕的测试时间。
针对第一个问题,可用一个实验回答。让两个在技术和经验上近似的团队,分别负责两个规模近似的项目,其中一个进行单元测试,另一个不进行单元测试的时间差。
下表是两个团队的进度和输出度量:
阶段
|
不进行单元测试的团队
|
进行单元测试的团队
|
---|---|---|
实施 | 7天 | 14天 |
集成 | 7天 | 2天 |
测试和修复缺陷 |
测试:3天 修复:3天 测试:3天 修复:2天 测试:1天 总计:12天 |
测试:3天 修复:1天 测试:1天 修复:1天 测试:1天 总计:7天 |
整体交付时间 | 26天 | 23天 |
客户发现的缺陷数 | 71 | 11 |
针对第二个问题,可引用一份研究报告,20世纪70和90年代进行的研究表明:通常,20%的代码包含了90%的缺陷。如果能找到这20%的代码,那么就能大大提升测试效率。
但困难的就是如何找到包含最多问题的代码。其实任何团队都能告诉你哪个组件问题最多,那你就可以从这个组件开始测试。
针对第三个问题,目前的办法是多写多测,让单元测试成为开发的一部分,不要苛求测试覆盖率,先就测试影响业务流程的核心代码。
二、测试替身
测试替身的作用是隔离被测代码,加速执行测试,使执行变得确定,模拟特殊情况,暴露隐藏信息。
其中隔离被测代码,使测试有针对性和容易理解,而利用测试替身实现的隔离,还有个副作用,那就是测试替身的速度要比本尊快很多。
测试替身的类型:
- Stub(测试桩):一个对象的所有方法只有一行,且各自返回一个适当的默认值。使用场景:只关心协作对象输送的响应。
- Fake(伪造对象):可以返回硬编码值,而每个测试可能需要有差异地实例化来返回不同值,模拟不同场景。使用场景:所依赖的服务或组件无法供测试使用,打桩产生了难以维护的糟糕代码。
- Spy(测试间谍):用于记录过去发生的情况,这样测试在事后就知道所发生的一切。使用场景:将其作为参数传递被测函数中。
- Mock(模拟对象):是特殊的Spy,在一个特定情景下可配置行为的对象。使用场景:关心某些交互,即两个对象之间的方法调用。
三、设计指南
- 避免测试中包含逻辑,不应该有switch、if-else等判断语句,for、while等循环语句。以免测试难以阅读和理解,难以复现,难以命名。
- 只测试一个关注点,一个工作单元只有一个最终结果,例如一个返回值、系统状态的一个改变或对第三方对象的一个调用。
- 专注检查行为而非实现,避免过度指定,只需检查最终行为的正确性即可,既不要使用多个模拟对象,也不要对一个被测对象的纯内部状态进行断言。
- 避免复杂的私有方法,不要直接测试 private 方法。
- 避免在构造函数中包含需要测试的代码逻辑。
- 避免单例,单例模式会妨碍创建不同的变体。
- 使用 new 时要当心,实例化的对象,应该仅限于不会替换为测试替身的对象。
四、测试坏味道
1)可读性
程序员用测试的方式来表达和验证代码的假设和预期行为。
阅读测试代码之后,就该理解代码应当做什么。程序员运行那些测试时,就该了解代码实际上在做什么。
- 问题:基本断言缺乏意义,因为断言的基本原理和意图隐藏在看上去无意义的单词和数字背后,造成难以理解。
- 改进:去掉魔法数字,改用断言方法,使用编程语言内置的 API 语法。
- 问题:过度断言很脆弱,并且掩盖了整体广度和深度之下的意图。
- 改进:识别无关细节并移除。
- 问题:人格分裂是指一个测试检查了多件事。
- 改进:去掉重复,将粗粒度的场景分离。
- 问题:过分保护是指在测试开头增加守卫语句和空值检查保护自己。
- 改进:去除冗余断言,检查要使真正的断言通过所需的中间条件。
2)可维护性
代码从不慢慢退化,而是直接奔溃。
测试也是如此,同样脆弱,程序员编写自动化的单元测试来尽可能地管理这种脆弱性。
大家都知道维护噩梦是什么,你绝对不希望你的测试代码沦落其中。
- 问题:重复最明显的是某一个数字或字符串在代码中反复出现。
- 改进:将可变数据提炼到局部变量中。
- 问题:残缺的文件路径会使代码无法转移,只能在某个人的计算机中。
- 改进:避免绝对路径,选择相对路径,用流来替换文件。
- 问题:像素完美出现的场景包括期望和实际产生的图像完美匹配。
- 改进:用适当的抽象层次来表达测试,将背后的细节隐藏到自定义断言中,进行模糊匹配。
3)可信度
软件开发其实就是在修改、演进和维护代码,如果不能信任测试,那么在即使看似最无辜的改动之后,仍然不能确信代码是否能够工作。
接下来会围绕测试不可靠的问题来检阅测试坏味道。
- 问题:永不失败的测试不具有价值,给你虚假的安全感。
- 改进:养成运行测试的习惯,例如临时修改被测代码来故意触发一次失败。
- 问题:轻率承诺是指测试实际上没有测试任何东西,或名不符实。
- 改进:确保断言了一些事情,确切找出要检查的行为,也更容易命名。
- 问题:降低期望就是降低了确定性与精确性的标准。
- 改进:提高门槛,使测试的期望更具体。
- 问题:有条件的测试是在一个测试方法内隐藏了秘密条件,使测试逻辑名不符实。
- 改进:确保测试在每个条件分支时都有机会失败。