测试驱动开发

2002年出版的书中 Test Driven Development: By Example 就有系统提到了。
更早地,1998年日本的山浦恒雄就在 How to Design Practical Test Cases 论
文中就明确指出先写测试用例可以极大降低软件缺陷率。到达客户端的软件软件
缺陷率是多少呢,0.02%。 这是什么概念? 假设一个软件在它的生命周期中总
的缺陷是10000个, 到达用户手中只有2个。 其它9998个都在出厂前就被发现并
且修复了。 可悲的是,国内用TDD的公司几乎没有听说过,“太高端了”,都在赶
进度,凭本能在做事。
在常见的软件开发过程中,我们把测试放在一个比较后面的阶段去做。 典型的
比如瀑布模型。在瀑布模型中,开发遵循如下过程, 需求分析-设计-实现-测试-部
署与维护。一步一步,测试放在了第四步,放在编码后面去做。 当然,程序员
在开发的过程中,也会有一些不系统的、零散的测试。瀑布模型一个理想的模型,
瀑布之水,不会回流。 但是在实际开发中,我们一定是要回溯的。经常地, 在
做后一阶段的工作时,发现前一阶段有些问题没有考虑到,或没有解决好,这时
不得不回溯到前面的阶段。造成返工。按照瀑布模型开发,客户很晚才能看到软
件。
严格遵循瀑布模型的项目几乎没有,也没有必要。这里我说一个我比较喜欢的开
发过程。首先尽量全面准确的收集客户需求,做一个需求池,便于在设计时整体
考虑,做整体设计。需求阶段会比较费时费力,目的是尽可能稳定设计。其实这
就是瀑布模型的第一个阶段, Requirements。总体设计完成后,把池子里的需求
按照重要性大小排个序,把最核心的需求排在最前面,进行编码,先做好这个需
求,交付给客户,让客户使用,尽早获得反馈。这里其实是一个小型化的瀑布模
型。客户反馈中很可能有需求变更,这时我们重新整理需求池子,对余下的需求
进行排序,接着对第二核心的需求进行编码,交付给客户,让客户使用,获得反
馈。如此循环,直到完成软件开发完成。这种方法的特点是,测试提前了,交付
频率高,客户开发者互动多,最后的产品用户满意度高,返工的可能性小。这种
开发过程还不是测试驱动开发。
这是开发过程,下面看看开发费用。 从饼状图我们可以看出,高质量的严肃的
软件,测试占了开发费用了一半,而编码只有1/6。现在很多公司,编码的花费
很多,而测试的花费很少。这不太好,除非你对自己的产品不负责任。 这样开
发出来的软件可能质量得不到保障,部署后遇到问题,花费更大。右边是冰山图,
我们看到, 开发费用只占软件总体费用的一小部分,维护费用占据了大头,是
冰山中沉在海面下的部分。Firefox, Ubuntu, Windows这些软件的维护费用就
是这个情况。
说到维护,如果一个软件的内部质量很差,维护起来是相当困难,甚至不可能。
维护包括因需求变更而新增加功能。所以,如果一个软件要长久维护,开发之初,
不是快速功能点实现就行了,而是要提高软件的内部质量,不这么做,急躁冒进,
以后想要修改软件时,花费更大。
越到后期,我们修复一个缺陷要花费的代价就越大,代价是指数级增长的。软件
的规模越大, 拖到后面修复缺点所需的代价就越大。图中显示的是Boehm给出真
实的数据。横轴是开发阶段,纵轴是修复缺陷的花费(注意到纵轴是log-scale
的,对数标尺)。这个图告诉我们要尽早发现问题,否则代价大到难以承受。如
果某购物网站在某重要节假日忽然出现一个无法支付的软件缺陷,而修复这个缺
陷花了4个小时,这样的损失是非常大的。测试驱动开发有助于我们尽早发现问
题。要十分重视测试。 去店里买衣服,我们总要到试衣间试试看,合不合身。
软件也一样,无测试,不软件。当然,完全没有测试的软件公司是没有的,关键
在于测试是不是做的充分,测试是不是做的聪明。尽量早的充分测试,有助与我
们增强对软件的信心,即时发现问题、解决问题,降低风险。在测试上做投资,
是很值得的,除非的你的软件根本就不重要。
下面我们来看3个实际发生的例子。第一个出现在我们学生开发的作业提交软件
中。大家仔细看这个加了黄框的文件路径,发现什么潜在问题没有?如果王莉同
时上两门课,并且每门课都有Assignment2,并且王莉上传的文件都叫
201631900112_王莉_A2.rar,这就会造成文件覆盖。
第二个例子还是这个作业提交软件,前几天我想加一门新的课,叫做
Introduction to Object-oriented Design and Analysis,结果爆出了
Course_Name too long这样的SQL错误。第三个例子是某个公司开发的某管理系
统,两个土地面积相加,出现的结果中,小数点后面出现了12位数字。这个系统
已经完成用户培训阶段了。
这些问题,都是在独立测试中发现的,我说的独立测试是指程序员不参与、由第
三方做的测试。如果这些问题暴露在客户面前,一方面会给他们造成不必要的麻
烦,或者“surprise”,他们麻烦了,就必然会来麻烦公司,另外一方面也是对公
司的信誉产生不良影响。
测试驱动开发怎么做?能帮助我们克服这些问题吗? 测试驱动开发, TDD, 要
“反直觉”, 要把这个测试工作尽量提前,提前到需求分析完成之后, 编写代码
之前,以至于编码之前我就考虑种种测试用例,各种可能出现的情况了。 采取
测试驱动开发过程的团队怎么想,反正最后总要测试,把测试提前能不能帮助我
们更顺利完成开发工作呢? 这个思想非常重要,非常有创新性。为什么呢?
就第一个例子来说,比如我在编码之前就给出这样一个测试用例:
王莉同学的学号是201631900112,她会同时选两门课,A与B,每门课都有
Assignment2,并且每门课王莉上交的文件名都是201631900112_王莉_A2.rar。
有这样的测试用例,就会逼迫我们去设计合理的文件存储目录结构。
第二个例子,比如在编码之前,我就设计如下三类的课程名:
空的课程名
长度是1-40的课程名
长度大于40的课程名
所以,测试驱动开发一个最重要的好处是让我们在编程时考虑问题更全面,降低出现问题的可能性。
在测试驱动开发的框架下, 测试准备工作从需求分析就要开始了。需求分析时,
每一个需求都是要可以被测试的,我们要为测试这个需求写下测试用例。这里是
说写下,不仅仅是想好。如果一个需求是含糊不清的,那么它就不是可以测试的。
可以测试的需求往往需要用数字表示,有具体的限定条件, “很快返回结果”,
很快是多快,不同人有不同的理解。
把测试工作尽量提前,最大好处的好处是“把较抽象的问题形象化,把较笼统的
问题具体化,把较模糊的问题清晰化”。 既然已经决定要先写测试用例了,下面
我们要决定测试用例的密度,按照日本日立公司山浦恒雄的经验,10行代码配套
1个测试用例就会有很低的客户端缺陷发生率。记住,是10:1。 1万行代码的软
件,需要配套1000个测试用例。这已经很多了,是不是这是日本东西质量高的一
个重要原因。 有的人说,可不可以是5:1,可不可以使15:1,当然是可以的,
看你对软件复杂度与你对软件可靠性的需求。 10:1这样一个比例其实是提供
了一个目标,有了目标就可以操作。
山浦恒雄列举了测试驱动开发的5大优点。
最后,我给大家讲一个用Python语言实践TDD的简单例子。 我在写这个测试脚本
用来测试remove_punctuation(在字符串中移除标点符号)的时候,
remove_punctuation这个函数还没有实现。所以,一开始运行这个程序,基本上
是没有测试用例通过的,屏幕一片红色。但是这些测试用例给出了非常具体的输
入与输出,在我设计程序时起到一个参谋的作用,使得我考虑问题更加全面。我
考虑了一般情况, 也考虑了异常情况(Undesired Events)。 比如输入是空字符
串,或是None,输出的结果是什么,都是清楚的。好处就在于明确,可操作,并
且给人一种“游戏通关”的刺激感觉。屏幕由红变绿,最后全绿,给人成就感。最
后,这个测试脚本是可以被重复利用的,可以做回归测试, 可以请别人去执行,
可以晚上我们睡觉时执行,等等优点。所以说,测试用例是有价值的投资,投资
的回报就是软件的质量提高、公司形象提升、员工成就感提升。
测试驱动开发的一个最大障碍就是急躁心理。欲速则不达。
Projects that aim from the beginning at achieving the shortest
possible schedules regardless of quality considerations, tend to have
the fairly high frequencies of both schedule and cost
overruns. Software projects that aim initially at achieving the
highest possible levels of quality and reliability tend to have the
best schedule adherence records, the highest productivity, and even
the best marketplace success. - Capers /'keipəz/ Jones
TDD是不是好方法?你在实践中用TDD方法吗?欢迎与我讨论。我的微博名字就是
蓝珲。