编写可测试的程序
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
说到自动测试,不少人都会想到单元测试框架(如cppunit/junit),或者gui测试工具(winrunner)。我想这是一种误 解,gui测试工具效果不佳是众所周知的,只有确实无法分离界面和实现,或者作为辅助手段时,才有必要存在。至于单元测试框架,这几年来它与自动测试如影 相随,但从客观上讲,它也只是一个简化测试程序开发的辅助手段。
自动测试与其说是测试的范畴,还不如说是设计的范畴。能不能自动测试,完全是由设计决定的,单元测试框架和gui测试工具的作用微乎其微。为设计良 好的模块编写自动测试程序非常简单,要不要单元测试框架完全是个人偏好。我以前开发过一个单元测试框架,但现在几乎很少用单元测试框架去编写测试程序。
如何编写可测试的程序呢?
1. 分离界面和实现。这是老生常谈的话题了,但说起来容易做起来难。我见过不少把MVC概念玩得很熟的人,真正写出来的代码却不像那回事儿。要真正精通MVC,还要多读多练,多思考,而不是拿着个概念在那里念念有词。
2. 契约式设计。契约式设计是一种非常好的思想,《Design By Contract 原则与实践》是一本好书(孟岩老师翻译的,翻译质量很高),它封面上的六大原则,每条原则的价值都远远大于该书的价钱。在C/C++中直接实现契约式设计 有些困难,不过这些思想为自动测试大开方便之门,其中有五条原则都可以应用于自动测试:1.区分命令与查询。2.将基本查询与派生查询分开。3.针对每一 个派生查询,设定一个后验条件,用一个或多个基本查询的结果来定义它。4.对于每个命令都写一个后验条件,规定每个基本查询的值。6.撰写不变式来定义对 象的恒定属性。
3. 注意模块的粒度。有的人确实把界面和内部逻辑分开了,但是内部逻辑成了一个大泥团,它的逻辑过于复杂,要想为它编写一个稍为完整的测试程序,那是相当困难 的事,即使写出来了,以后维护也成问题。设计的主要过程就是责任的划分,把每个类的责任划分清楚了,系统的对象模型也就出来了,这也是最考究设计者功力的 地方。尽管我们有一个万能良方作为指导:每个类承担单一的责任。但在真正设计时,可不像那么简单。
4. 减少模块之间的耦合。我见过几个大系统,里面的模块耦合非常紧密,甚至从源代码重新build整个系统都不可能。为了编译它,要先用一些空函数生成一个 lib文件,以便让一部分模块先链接过去。这些系统没有GUI界面,不用费心去分离界面和实现,但是自动测试仍然很困难,原因是模块之间耦合太紧了,运行 一个测试程序就相当于运行整个系统。减少不必要的耦合并不难,分层设计就是最好的方法之一,各层之间以标准的接口进行交互,上层可以直接调用下层的服务, 而下层只能通过消息或者回调函数与上层通信。另外,像访Visitor/Builder等设计模式也能很好的解开模块之间的耦合。其实,归根结底就一句 话:针对接口编程。
5. 控制随机因素。自动测试并不是简单的把测试程序运行一遍,它要检查运行结果是否与期望相符。如果你根据不知期望的结果是什么,还谈什么自动测试呢?然而系 统中往往有些随机因素,这些随机因素不是指内存越界或者未初始化的变量,而是指一些有意加入随机因素,比如在游戏中使用的随机数,它们会影响我们对测试的 结果的判断。这时我们可以包装一下系统的随机数函数,让我们可以得到上一次的随机数,以便对期望结果进行估计。另外像网络信号、线程间通信和并发也存一定 的随机性,要尽量减少它们对测试结果的影响。
6. 为支持测试提供额外的函数。大多数情况下,一个模块只会实现它必要的功能,这是应该的,保证接口最小化。但这也为我们检查期望值造成了障碍,有时我们无法检查一个操作的具体影响,不要再犹豫了,为它增加一些查询函数吧,这些工作量会由于减少调试时间而得到回报。
对于自动测试,不要被XP和单元测试框架这类概念搞迷惑了,简单一点,先做起来,再完善它,否则它永远都只是个美好的愿望。自动测试也不是灵丹妙药,不要对它期望过高,它不能解决所有问题,但它确实很有价值。