主题:【原创】闲聊敏捷编程——测试驱动开发(一) -- 代码ABC
几乎所有的敏捷编程方式都会采用测试驱动开发过程。所以我先聊聊测试以及测试驱动开发。
软件测试有一句名言——就算是非计算机专业大多也曾听过。原话是:“程序测试是表明存在故障的非常有效的方法,但对于证明没有故障,测试是很无能为力的”。这句话是图灵奖——计算机界的炸药奖——获得者Edsger Wybe Dijkstra(这个名字饶舌得很)说的。而魔鬼的翻译就是再严密的测试也不能证明程序的正确性。魔鬼接着说,但是,为了避免一些愚蠢的错误,你必须进行测试!当然,那些无法避免的错误也是愚蠢的。
无可奈何的是所有软件工程的书都会强调测试的重要性,同时会花相当的篇幅讲解各种测试方法。当你按照软件工程要求组织测试的时候,你会发现你走进了魔鬼的乐园。测试是要花时间的,花人力的,也就是说成本高昂。一般项目的测试开销和开发的开销是同一量级的。精力花出去了,而回报是不能证明程序是正确的,这无论如何都是一件令人沮丧的事情。当你准备咬牙认命的时候,你有可能听到魔鬼嘲讽的声音——你会测试吗?事实上许多程序员(包括有些年头的程序员)是不懂如何写测试的,或者不懂得如何写出方便测试的代码。
其实魔鬼是有弱点的,弱点就在那句关于测试的名言。测试驱动开发用一种智慧的手段推翻了那句话,也就是测试是用来证明程序是正确的!它的逻辑很简单:所有需求都是可以测试的,凡是不能测试的需求都是无法实现的。所以只要程序通过了这些测试那么程序就是正确的。那么如果测试不严密怎么办?很好办,这证明了测试是错的,程序没错!这实在是一个无法打败的逻辑,很伪科学,是吧。其实这是一种智慧。
我曾经写过一篇文章(编程随想)说写程序就是一种翻译过程,是将人们的想法翻译成代码的过程。由于用来描述想法的语言是有冗余的,而代码是无冗余的。那么在翻译过程就不可避免地会引入错误。为了减少这种错误,计算机的专家们开发许多工具和描述方式。软件工程学则规定了翻译的步骤以及每个阶段成果的格式。比如一个想法——或者说软件功能,首先要变成需求文档,再变成概要设计文档,再变成详细设计文档最后才变成代码。在这种过程中自然语言变成代码需要好几个步骤,每个步骤都可能引入错误。所以我们可以指责需求分析错了、概要设计错了、详细设计错了、代码错了。而测试驱动开发不同,它没有所谓的需求文档,而是直接把需求变成测试代码。因此如果程序通过了测试,出现错误的地方就只能是测试。注意这里测试就是需求。搞明白了这点就算是对测试驱动开发入门了。
有人会说,这和一般的软件工程的测试没有什么本质的区别嘛。软件工程是根据需求文档来编写测试的。而测试驱动开发只不过是将需求文档编写步骤省去而已。而且这样还增加了测试编写的难度。
在前一篇闲聊我说过从软件工程到敏捷开发是一个螺旋上升的过程——或者文绉绉地说叫扬弃,也就是说不少软件工程的概念被继承下来了,但他们不是一成不变的拷贝而是进化成另一种形式。比如需求分析现在进化为测试代码的开发。而测试则和代码开发以一种新的方式结合起来。
我们知道许多想法描述起来是复杂的,甚至很难用一个定义表述清楚。文科生别拍我,这里要求定义是清晰无歧异的,并且所使用的概念和规则是自洽的。不难吗?那么谁能定义西西河的认证用户是什么?
啊!认证用户!谁捞我出来?——某个很有残念的定义。
认证用户是一群不容易惹麻烦的用户——铁手的定义。
认证用户是由不小于15个认证用户赞成,不大于8个用户反对的用户。——循环定义。
认证用户是一种特权用户。——嗯,有点味道,但是什么是特权用户呢?
认证用户回帖不用验证。——好了,说到点子上了。
通常这些想法的描述会变成对概念外延的描述,也就是说像上面的定义可以描述为:如果一个用户回帖不用验证,那么这个用户就是认证用户。注意这个句式和最后那个描述的不同,这里是一个条件判断描述。也就是我们把一个想法——功能需求变成了一个测试。我们通过转换概念外延的描述构造出一系列的判断,据此定义一个概念或者一个功能。通常为了定义一个概念或功能需要若干条判据。因此我们可以写下若干个测试。不过在测试驱动开发中这些测试不是一股脑写出来的,而是逐个写出。每写出一个就要写出能够通过这个测试的代码。这样做的目的是降低分析的难度,并且在构造代码的过程中保持思路的清晰。比如完成回帖验证的判断后,我们会想起认证用户还需要在其页面上加一个图标。这样我们的程序就在这一个个测试中丰满起来。
打个有点火药味的比方。以前的方法就像开炮:方位0110,距离16000,榴弹发射!——这是需求描述。然后看看命中了没有——这是测试。没命中!向左修正10,距离修正100,发射!这还是固定目标——需求没变。移动目标就更费劲了。
测试驱动则向导弹,发射的时候只要大致对准就可以了,在飞行过程中不断有修正指令——逐步发掘出来的测试。这样不管固定目标还是移动目标,通杀。
好吧,我承认现实不是那么美好的。
我承认这种测试也是不完美的,它也会漏掉一些关键的内容。甚至本身就是错误的。但至少我们把可能出错的地方限制在一个仅此一个地方上了。这就是进步。至于如何解决上面的问题,我们下回分解。
本帖一共被 2 帖 引用 (帖内工具实现)
你的那个“翻译”的说法,官方名称叫MDA
是哪个MDA啊?我现在很怕缩写。
XP包含整个开发流程; MDD 只涉及分析和设计
又得一个10年
个人是不需要的,大致上3个月就能上手,然后就会发现没有测试就浑身不舒服。
就我看到的情况是,主要的阻力反而在客户这边,他们被软件工程/CMMI什么的洗脑地厉害,一时半会还真转不过来。
现在敏捷方法是软件工程学的热门话题,但传统的瀑布模型仍然是主流的开发方法,希望楼主能谈一下两种方法各自的优缺点和适用范围。
树立几个典型,俺们以后学习学习就容易实施了。
其实UP也是迭代过程,只不过迭代的尺度比较大而已。
在下也搞过敏捷开发,当时老大就让我们写代码前先写测试用例,号称测试驱动开发,说实在的我也不知道这个定义对不对,但还是老老实实按这个去做了,实践中发现它确实有效,你想想,测试用例比代码还先完成,测试员必然要仔细整理自己的思路,不容易出现以前那种需求和功能不一致的情况,而且每写一点代码都是通过测试的,代码质量比较高。不过,发现了两个问题,罗列如下:
1.特别的费时费力,既要写代码又要写测试例,工作量是以前的两倍(其实还不止),虽然提高了代码质量,但这是以加班时间换取的,投入产出比没有仔细算过,但那段时间公司的弟兄们叫苦连天那是真的。
2.测试例集中在单元测试方面,缺少集成和系统的测试。也就是说大家写的单个函数基本上都没有问题,但是联合在一起运行的时候就出问题了,不是接口格式错,就是类型不匹配。一通大改以后,好不容易可以集成运行了,可大家对这个程序还是缺乏信心,还需要专职qa的介入。而且感觉这个问题在不做测试驱动开发的时候还不突出,反倒是推行了新的工作模型后才集中出现,我想可能还是因为写测试例的过程耗费了大家太多精力,无力去关注接口和集成方面吧。
后面会讲到,核心是一点测试也是设计,在写测试前必须考虑如何把代码写得方便测试。其结果就是要设计好接口。
速度问题也有点复杂,如果你把测试代码花的时间作为一种投资来看的话,那就能明白TDD和敏捷的主要思路了。
另外,测试驱动开发不止单元测试,还包括集成、系统层次的测试,这些也是必不可少的。
因为都是软件开发组织的方法论。优缺点比较就复杂了,我个人感觉是和用户的合作是敏捷的一个软肋,一方面它颠覆了不少以往的经验,另一方面对客户合作的要求很高。还有很多,容我慢慢说明