TDD(Test-driven development),也就是我们常说的“测试驱动开发”,是由 Kent Beck 在1996年提出的概念。TDD这个术语,经常被人挂在嘴边,然而真正在项目实施,却寥寥无几。
是TDD对开发者要求太高?还是TDD根本就不值得去做?
非也。为了让大家对TDD有一个具体而亲切的认识,我先给大家举一个在编程中使用TDD进行开发的实际例子。
这是一道计算保龄球比赛一局总得分的编程题,保龄球的计分规则非常简单:
题目要求我们提供一个名字为Game的类,这个类有两个方法:
下面开始使用TDD来完成这个编程训练。
如果此时你已经在开始构思要如何实现,请打住!因为这不是TDD的风格。
记住,先别想着怎么去实现,先写测试用例,也就是先把你调用这个Game类的代码写下来。
首先,我们创建一个BowlingGameTest类:
import junit.framework.TestCase;
public class BowlingGameTest extends TestCase {
}
接着添加第一个测试用例:
当我们刚刚new了一个Game对象时,编译器就提示错误了,此时暂停测试用例的编写,开始编写产品代码!(这么做似乎有点过于耿直,不过对于加深对TDD的印象还是很有帮助的)
我们创建了Game类,此时编译通过,执行所有单元测试,绿条!
接着我们在第一个单元测试中调用roll方法和score方法,同样的,我们遇到编译不通过的问题,再依次给Game加上对应方法后,我们得到了下面这段代码:
我们心里很清楚,这个代码是经不住考验的,我们随便添加一个单元测试,都可以让测试用例不通过。比如我们让一个保龄球世界排名倒数第一的球手去比赛,每轮他都只击倒一个球瓶,测试用例毫不犹豫地失败了:
于是我们要修改一下逻辑,在每次roll的时候,加上分数。细心的读者可能还发现了,下面这段代码还对测试代码进行了重构,把每个单元测试都要做的new Game()操作抽取到了setUp方法中:
接着我们又发现我们经常要模拟很多次击倒相同数量球瓶的操作,因此我们把这个操作抽取成一个rollMany方法:
我们的代码到这里还完成不到1/3,但是我们已经做了两次重构,没错,TDD的过程,也是不断小步重构的过程。
接下来,我们测试一下Spare的场景,这一次测试用例又理所当然的失败了(不要担心一次次失败会打击自信心,因为这些都是我们刻意制造的失败,人们面对意料之中的失败往往更有激情)。而当我们准备动手修改产品代码时,却发现了一个代码设计层面的问题,那就是我们在roll方法里面做了roll不应该做的事情,roll意味着扔球,而我们却在里面修改了得分:
此时我们需要把新增的用例暂时屏蔽掉,然后对产品代码进行重构,roll专心做它的事,把计算得分的活交给score来做:
接下来,就是继续放开我们之前屏蔽掉的测试用例,继续修改产品代码,让测试用例通过,然后再添加STRIKE的测试场景、添加更多的测试场景…… 这些过程就不再赘述了,因为作为一个TDD的例子,前面这几个步骤,已经足够让大家对TDD有一个比较深刻地理解了。
关于这道题目的完整解答过程,大家可以到Bob大叔的TheBowlingGameKata去下载对应的PPT。
上面的保龄球训练中,我们一直在遵循着TDD的三项法则:
遵循着三项法则,我们开发的过程就是下面这五个步骤不断循环、小步迭进的过程:
TDD带来的最大好处是提高了单元测试的覆盖率。 传统的先写产品代码,再写单元测试,有两个弊端:
那么提高了单元测试的覆盖率对产品有什么好处呢?健康的单元测试覆盖率是在90%以上,为什么要那么高?高覆盖率带来的好处主要有以下三点:
除了提高单元测试的覆盖率,TDD还能够促成良好的代码设计。由于你先写测试代码,你会尽可能的让代码调用起来更加简单方便,这也就促使你去考虑如何更好的设计代码。如果不先写测试,最后很有可能就会出现一个函数里实现的功能过多,或者和其他代码过于耦合而无法测试的情况。
在学习一项技术时,总是要提醒自己——“没有银弹”,任何技术都有其局限性。然而,由于用TDD的人实在不多,在网上搜了很久,也看不到什么特别有建设性的观点。下面是我找到的一些关于TDD局限性的看法:
回到这篇文章的主题,“如何说服你的同事使用TDD”,首先,你被我说服了么?
如果你的回答是Yes,你被我说服了,你打算开始使用TDD,好,下面我给出一些练习和使用TDD的建议。
1)TDD Katas 训练
先不要急着在工作中去使用TDD,Bob大叔的网站上还有很多跟保龄球训练类似的题目,你可以去练习一下。而且,相同的练习可以反复训练,Bob大叔在他的《程序员的职业素养》里是这么说的:
和习武者一样,程序员应该懂得很多种不同的卡塔,并定期练习,确保不会淡化或遗忘…
真正的挑战是把一个卡塔练习到炉火纯青,你可以窥见其中的规律。要做到这一点可不容易。
下面是Bob大叔推荐的卡塔:
Bob大叔的主页上还有很多其他的Kata,大家可以上去探索探索。
2)编程题训练
网上TDD的例子确实有限,但编程训练题却是一大把,很多网站也都收录了许多经典的算法和数据结构的题目,我们完全可以使用TDD来对付这些题,把代码提交上去,如果没有通过,就说明自己的测试用例不全。
使用TDD来对付编程题,非但不会影响你的答题时间,反而让你一小步一小步的完成题目,而不是像以前一样,思考了很久,却一行代码都没写出来。
网上提供在线编程练习的网站很多,我自己现在在用的是LintCode,大家也可以去自己喜欢的网站上练习。
3)面试时使用TDD
面试时,如果面试官让你在纸上写代码,那就给面试官show一下TDD吧,将纸张一分为二,一半写测试用例,一半写实际代码,当然,你可以先写伪码,因为总免不了要重构,全部写完再用实际代码写一遍,交给面试官。
同样的,TDD可以缓解你一行代码都写不出来的紧张心情。
4)运用到实际项目中
实际项目中运用TDD,通常不像做编程题那么轻松,你可能需要使用Mock/Stub之类的东西,不过这些都有现成的代码库,比如Spring就提供了一套很方便的测试库,你只需要花费一点时间了解一下如何使用即可。
如果你有个开明的领导,不妨在做TDD之前跟他说一下,说不定他和你有一样的想法,并且会给你一些支持;如果你的领导看起来并不那么开明,你觉得有很大可能性他会禁止你TDD,那就别告诉他,悄悄执行,如果最后确实有成效,跟他说,并且要求在组内做一次技术分享,如果最后没什么成效,也不要紧,领导看到的只是你测试用例非常完善的代码。
好了,我似乎又走题了,到底“如何说服你的同事使用TDD”,很简单,用实际行动告诉他们!如果你在使用了TDD之后,确实提升了代码质量,降低了代码缺陷率,那么请理直气壮地和他们分享你使用TDD的心得。
TDD是专业人士的选择,它是一项能够提升代码确定性,给程序员激励、降低代码缺陷率、优化文档和设计的原则。对TDD的各项尝试表明,不使用TDD就说明你可能还不够专业。 —— Bob,《程序员的职业素养》