本书内容关于如何有效处理遗留代码,遗留代码是指没有编写测试的代码。因此,为遗留代码编写测试是改善遗留代码的首要任务。对一个大系统,不可能从头开始编写每一处的单元测试,一般只能从当前需要改动的地方开始,逐步添加单元测试,形成“软件夹钳”,进而修改并改善现有代码。遗留代码修改算法:
(1) 确定改动点;(前提:理解代码)
(2) 找出测试点;(前提:理清代码间的联系)
(3) 解依赖;(解依赖是为类编写单元测试的前提 )
(4) 编写测试; (编写符合代码当前行为的特征测试 )
(5) 修改、重构。 (在存在测试覆盖的前提下,修正bug 、改善设计等 )
从上述算法可以看出,前4 条是关于如何编写测试代码的,而解依赖是编写测试的前提,因为本书很大程度可以说成是关于如何解依赖的书籍,书中也用来很大的篇幅来介绍解依赖技术。当然,解依赖除了处理遗留代码,还可用于指导编写易测试的代码。
测试代码的命名约定:(测试类:DBEngine )
单元测试类:一个类至少要编写一个相应的单元测试类,故单元测试类常常在目标类名上加 “Test” 前缀或后缀。为便于浏览,加后缀方便些。DBEngineTest
伪类(伪对象或仿对象):伪类是指用于测试的伪造类。 伪类常用”Fake” 作为前缀,使得所有的伪类都在一起,便于区别。FakeDBEngine
测试子类(testing subclass ): 测试子类是指利用继承将不关心的行为架空使只访问测试所关心的行为的派生子类。 测试子类常由子类化并重写方法技术生产。测试子类本质就是为测试类接触不必要的测试依赖。测试子类常使用“Testing ”前缀。TestingDBEngine
遗留代码工作的三个关键概念:感知、分离和接缝 。
感知和分析和解依赖直接相关,解依赖是将类放入测试用具的重要手段( 有时是唯一手段) ,因为类之间往往是相互依赖,相互影响的,为了能单独测试某类,我们需要接触类之间的依赖关系,尤其是测试类所依赖的类。很多时候解依赖唯一的办法就是通过伪装成被影响的类来直接感知所受到的影响。感知和分离式解依赖的两个目的:1) 感知;当无法感知代码的状态 ( 测试) 时,通过解依赖来感知“状态”;2) 分离:当无法将代码放入测试用具时,通过解依赖来分离测试代码。
类难于测试的根本原因在于:类很少是单独存在的,往往是相互依赖。所要测试的类往往存在如下依赖关系:1) 实例对象(成员变量); 2) 委托对象(接口参数); 3) 临时对象(所创建或实例化的任何对象); 4) 全局对象(单体,系统或库API )。
注:上述对象不包括基本类型和基本对象(如STL 等标准库)。
所有的对象都存在自己的逻辑,从而如果想要编写单独的类测试用例就应该接触对这些对象的依赖。下面一一给出解决方案。
1) 实例对象(成员变量):使用伪对象
2) 委托对象(接口参数):使用伪对象
3) 临时对象(所创建或实例化的任何对象):使用伪对象
4) 全局对象(单体,系统或库API ) :以获取方法替换全局引用;封装API ,形成内部调用接口,通过子类化并重写方法,屏蔽不必要的系统API 调用。
使用伪对象的解依赖技术:
参数化构造函数:直接传入伪对象。
参数化方法:避免临时对象的硬编码,直接传入伪对象
引入实例委托:避免全局对象的硬编码,直接传入伪对象
替换实例变量:增加实例变量设置接口,不推荐。
引入静态设置方法:用于替换静态对象, 如单体实例。
参数适配:将依赖的参数类型替换成可伪装的自定义类型。
实现提取:用于提取抽象基类或接口 ,进而定义伪对象Fake
接口提取:用于提取抽象基类或接口,进而定义伪对象
接触类内部依赖的解依赖技术:
子类化并重写方法 :将少数无须测试的内部接口重写,通过实例化重写的派生类来测试原有类的接口。这里的重写是指虚函数的重实现,而非覆盖。
提取并重写调用:封装API ,并重写该接口
提取并重写工厂方法:用于实例化伪对象,避免更改接口,本质就是替换实例变量。
提取并重写获取方法:延迟获取实例变量,用于C++ 。因为C++ 在构造函数中虚函数机制被禁止,故无法再构造函数中调用子类重写的工厂方法。
特定情况下的解依赖技术:
分解出方法对象:重构巨型方法
朴素化参数:避免参数依赖
封装全局引用:
以获取方法替换全局引用:利于重写获取方法。
定义补全:C/C++ 的定义和实现是分开的,通过重写实现替换原有行为。
连接替换:利用链接期接缝,替换库,DLL 等,实现行为替换。
暴露静态方法:避免对象的实例化,用于难实例化的对象。
换函数为函数指针:用于C 的行为替换,不推荐。
写于2008-5月