软件界旷世之架:测试驱动开发(TDD)之争
摘要:在软件行业中,神仙打架的名场面,那就不得不提的是2014年的那场——测试驱动开发(TDD)之争。
在历史上有很多精彩绝伦的神仙打架,比如数学界的牛顿和莱布尼茨关于微积分的旷世之争;比如量子物理中的爱因斯坦和波尔的紫禁之巅;比如足球里的梅西和C罗的旗鼓相当难分高下;又比如滴滴和快滴之间触目惊心的烧钱大战……而在软件行业中,也同样有神仙打架的名场面,那就不得不提的是2014年的那场——测试驱动开发(TDD)之争。
比赛的红方是David Heinemeier Hansson,蓝方是Kent Beck。David Heinemeier Hansson 由于名字较长简写成DHH,Ruby on Rails 正是出自于DHH之手。而这场打架还加入了“裁判”员——Martin Fowler,在比赛中Martin Fowler记录了红蓝双方的每一次组合拳、上勾拳、侧踹、抱摔……总结如下:
红方DHH观点:
许多推动TDD的开发人员会让你觉得:如果你不使用TDD的话,你的代码就是肮脏的。
由单元测试开始驱动你的设计并不是一个好的主意。
TDD的概念流量交易“测试必须够快”是目光短浅的。
对TDD的依赖会导致彻底忘记系统测试。
关注并且只关注单元模块并不能有助于创建一套完美的系统。
100%的覆盖率是愚蠢的。
程序员希望软件是一门科学,可是它并不是。它更像是创造性的写作活动。
优秀的软件并不像工程学那样,它更像写作。清楚简洁的写作要优于复杂晦涩的写作。
清晰是有好处的,好到应该将清晰性作为第一目标,而非测试覆盖度或者测试速度。
成为一名优秀的开发人员就像成为一名优秀的作家一样困难。
就像写作一样,成为优秀的程序员的办法就是以清晰为目标从而大量编写软件、大量阅读软件。
蓝方Kent Beck观点:
DHH已将TDD委托给历史垃圾堆。我很难过,不是因为我就把它从历史的垃圾堆中拯救出来,而是因为现在我需要雇佣新技术来帮助我解决编程过程中的许多问题:
过度工程化。我倾向于“投入”我“知道”我“将需要”的功能。使一个红色的测试变为绿色(以及未来的测试列表)有助于我实现足够的功能。我需要找到一个新的方法来保持专注。
API反馈。我需要找到一种新的方法来获得关于我的API决策的快速反馈。
逻辑错误。我需要找到一种新的方法来抓住那些我很容易犯的讨厌的测试错误。
文档。我需要找到一种新的方式来传达我对api的期望,并记录我在开发过程中的想法。
感到不知所措。我真的会怀念如何使用TDD,即使我无法想象一个实现,我几乎总能想出如何编写测试。我需要找到一个新的方法,以便下一步上山。
将接口与实现思想分离。我倾向于用实现推测来污染API设计决策。我需要找到一种新的方法来分离这两个层次的思维,同时在它们之间提供快速的反馈。
协议。我需要找到一个新的方法,精确地与一个编程伙伴关于我正在解决的问题。
焦虑。也许我最怀念的是TDD给我的瞬间“一切都好吗?”按钮。
我相信我会找到其他方法来解决这些问题。及时。疼痛会减轻的。再见TDD,老朋友。
神仙打架不亏是神仙打架,从那以后业界关于测试驱动开发的观念也分成了两派。一派主要来源自像国内的一些互联网等项目中声音——需求的迭代和更新之快,要求公司或团队能快速交付有价值的产品,而TDD对于很多开发人员来说无疑是带来了繁重的工作压力和交付压力。甚至有人开玩笑话说:“ Deadline Driven Development 才是第一生产力 ”。
当然也有人力挺TDD,“TDD并没有死。很明显,既然它有这么这么多的支持者,它怎么可能会死呢? 这就像在问,设计模式死了吗?或者功能性自动化死了吗?不,它并没有死。而且它在将来任何时候都不会死亡。它将来可能会变成其他一些新的事物、甚至是一些更好的事物,但是它永远不会死亡。所以让我们跳过这一部分吧。”
关于测试驱动开发说了这么久,那么测试驱动开发到底是个啥呢?
测试驱动开发(TDD)是什么
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。 它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。 这有助于编写简洁可用和高质量的代码,并加速开发过程。
Kent Beck:“测试驱动开发不是一种测试技术。它是一种分析技术、设计技术,更是一种组织所有开发活动的技术”。
分析技术: 体现在对问题域的分析,当问题还没有被分解成一个个可操作的任务时,分析技术就派上用场,例如需求分析、任务拆分和任务规划等,《实例化需求》这本书可以给予一定的帮助作用。
设计技术: 测试驱动代码的设计和功能的实现,然后驱动代码的再设计和重构,在持续细微的反馈中改善代码。
组织所有开发活动的技术: TDD 很好地组织了测试、开发和重构活动,但又不仅限于此,比如实施 TDD 的前置活动包括需求分析、任务拆分和规划活动,这使得 TDD 具有非常好的扩展性。
测试驱动开发(TDD)的目标
Kent Beck 在他的著作《Test-Driven Development》(见参考附录)一书中提到:“代码简洁可用这句言简意赅的话,正是 TDD 所追求的目标”。
对于如何保证“代码简洁可用”可以使用分而治之的方法,先达到“可用”目标,再追求“简洁”目标。
可用: 保证代码通过自动化测试。
代码简洁: 在不同阶段人们对简洁的理解程度也不一样,不过遵循的原则差不多,例如 OOD 的 SOLID 原则(详见参考附录),Kent Beck 的 Simple Design 原则(详见参考附录)等。
虽然有很多因素妨碍我们得到整洁的代码,甚至可用的代码,无需征求太多意见,只需要采用 TDD 的开发方式来驱动出简洁可用的代码。
测试驱动开发(TDD)的规则
在TDD 的过程中,需要遵循的三项原则:
在编写好失败的单元测试之前,不要写任何产品代码。
只要有一个单元测试失败了,就不要再写测试代码。无法通过编译也是一种失败。
产品代码恰好能够让当前失败的单元测试成功通过即可,不要多写。
测试驱动开发(TDD)的流程
测试驱动开发是一个过程,依赖于不断重复极短的开发周期,这个周期也称为“红灯-绿灯-重构”,如上图。简单的来说,基于TDD的三项原则,TDD的这种步骤(周期)如下:
添加一个小的测试
运行测试并查看失败
对测试进行微小的改动通过测试
运行所有测试并看到其通过
通过重构去掉重复部分
需要注意的是,不同阶段有不同的目的,他们需要不同的解决方案,前二个阶段需要很快地完成,以便知道新添加功能的状态。为了达成这个目的,可以通过任何手段,因为仅在这时才这样做,也是为了能快速完成好的设计。
测试驱动开发(TDD)的好处
TDD主要的好处主要包括了,确定性、重构代码、单元测试即文档。
确定性。TDD提升了单元测试的覆盖率,在每轮迭代产品都会新增代码,如果有一套覆盖率很高( 90% 或更高)的单元测试,那么只需执跑一遍测试用例,那么能成功交付的把握就会比较大。反之,如果覆盖率越低,越需要更多的人力去进行手动验证。 在 kent Beck的《测试驱动开发》举的例子中,正因有了TDD才有勇气和老板说我们可以做!这就是TDD最强大的地方,它让你拥有一套值得信赖的测试,打消你对修改代码的恐惧。
重构代码。Martin Flower在他的《重构》中也指出,完善的单元测试是他进行重构的基石,从TDD的流程可以看到,重构是TDD的一部分,运用TDD的同时也推动了代码的重构。
单元测试即文档。在软件行业里,人员的变动的很频繁的,如果要尽快熟悉某个模块的业务逻辑。看文档?程序员写的文章一般都不太容易看,而且文档经常会和代码不同步,代码修改了文档没跟着改的事情经常发生。看源码?看完也不一定知道为什么。如果这时候有一套非常完整的单元测试,那可能就是所有接手别人代码的程序员的福音。首先,代码不会撒谎,其次,测试用例明确告诉了你这个函数是做什么的,什么输入对应的都有什么预期输出。单元测试就是最好的底层文档,哪个专业人士不想提供这样一份文档呢?
此外,TDD还能够促成良好的代码设计。由于你先写测试代码,你会尽可能的让代码调用起来更加简单方便,这也就促使你去考虑如何更好的设计代码。以避免会出现一个函数里实现的功能过多,或者和其他代码过于耦合而无法测试的情况。
当然测试驱动开发除了好处以外,还有神仙打架中红方代表DHH所提出的一些问题。总结来看,关于TDD的争议可以大致从这几个方面来看,软件开发应该由什么来驱动,测试的速度和覆盖程度,以及设计思想层面等几方面。从 辩证统一的角度来看,事物有两个方面, TDD不一定能适用于所有的场景,同样TDD的局限性在某些场景下也不见得是对的,如果想要能更好的适用于自身,不仅要拿捏好度的问题还要以敏捷的思想来应对问题,比如不应该盲目的制定100%或0%的测试覆盖率,也不应该固化开发步骤而不顾实际情况。
所以,在最后的神仙打架中,Kent Beck也表达了David的论述可能会让TDD浴火重生、凤凰涅槃的观点,希望可以找到更加好的方法。但无论如何, 在我们实际工作中,不应该因为某些 观点成为我们接受或者拒绝它的理由。正所谓大道甚夷,而民好径,作为敏捷开发中的一项优秀实践来看,TDD只有在真正使用过后才能评价是否已死的问题。那么你在践行敏捷开发的时候,是否使用过TDD这种实践呢,又或是践行过其他一些敏捷开发的实践呢,有没有评测过你所在的项目中的敏捷开发的成熟度是如何的呢?
没有那就对了!