TDD简介
TDD是Test-Driven Development的缩写,即测试驱动开发。TDD的基本思路是利用测试来推动开发的进行,并不是单纯的测试过程。TDD是极限编程的核心之一,但TDD也可以单独运用。TDD具有:明确需求、明确设计、形成文档、自信编程、提高效率、强制测试等优点。
Object Meentor公司总裁,极限编程领域资深顾问Robert C. Martin提出了TDD三条军规,这三条军规简单明了地阐述了TDD过程:
1. 除非这能让失败的单元测试通过,否则不允许去编写任何的产品代码。
2. 只允许编写刚好能够导致失败的单元测试(编译失败也属于一种失败)。
3. 只允许编写刚好能够导致一个失败的单元测试通过的产品代码。
TDD有很多优点,但也有可改进之处,主要在三方面:自动化程度低、对资源利用不充分、干扰编程思维。
自动化程度低
TDD要求先编写测试代码再编写产品代码,这个编码顺序决定了难于利用工具来生成测试代码。当然,工具也不可能根据测试代码来生成产品代码。如果首先编写产品代码,工具则可以自动生成大部分测试代码,人工一般只需要设定用例的输入输出就可以了。测试代码的编写工作量是很大的。
TDD要求先编写测试代码的理由主要有两点:一、这是设计行为,可以明确代码需求和设计;二、强制测试,避免先编写产品代码后,忽略测试。
编写测试代码确实是一种很好的设计行为,但是如果进一步分析,“设计”主要体现在设定用例的输入输出上。假如我们不编写测试代码,而是用表格列出每个用例的输入输出,设计效果与编写测试代码差别不大。
在编写一个类的测试代码前,难道不需要在心里想清楚类名是什么,父类是什么?在编写一个函数的测试代码前,难道不需要想清楚函数原形,如函数名,返回类型、参数个数、参数类型、参数名?如果这些不确定,如何编写测试代码?在心里想和写出来有本质区别吗?
如果先编写产品代码的框架,包括类定义和函数原形,则可以用工具生成大部分测试代码。在编写函数的具体实现前,再定义用例,然后再编写代码,其结果和纯粹的TDD有什么区别呢?但效率则可以大幅提升。
至于强制测试,如果需要强制,那就说明单元测试对程序员并未形成足够的吸引力,使程序员自愿自觉地测试,否则就不需要强制了。TDD并没有将单元测试对开发过程的促进作用充分发挥出来,对单元测试产生的资源利用不充分,而且有可能干扰编程思维,后面进一步讨论。
对资源利用不充分
单元测试的输出可以完整描述程序的行为。程序行为就是在什么输入下,会执行哪些代码,会产生什么输出。写代码时如果能随时察看程序行为,就比较容易想明白思路对不对,接下来应该怎么写,容易找出错误原因,不但效率高得多,也没那么累。
只要做了单元测试,反映程序行为的数据就一定会存在,这些数据是一种宝贵的资源,工具可以自动捕获,从而让程序行为可视,TDD忽略了这一点。一般的开源框架,只对输出数据进行了判断,只显示测试是否通过,不能显示所有的输入与输出数据,也不能显示用例所执行的代码。
很多时候,实现一个用例对应的功能点是很复杂的,代码可能有几十行,只有等到该功能点写完,程序员才知道测试是否通过。在编写一个功能点的过程中,程序员可能希望每写几行代码,就看看程序做了什么,以便检查和调整思路,找出和修改错误,这是TDD做不到的。
如果利用单元测试,使开发过程中程序行为可视,对程序员就会形成强大的吸引力,从而自愿自觉地进行测试。
干扰编程思维
程序员是坚强的,而灵感、创意、思路则是脆弱、易失的。编程工作需要连贯的专注。TDD过程中,编写测试代码大概占用一半时间,而且与编写产品代码交替进行,难免影响编程思维的连贯性。当遇到难题,测试工作卡住时,对开发思维的干扰就更大了。干扰思维可能会造成程序员本能的抵制,这大概也是很多程序员不愿意编写测试的真正原因吧。如果TDD结合自动化,让工具完成大部分测试准备工作,程序员只需要设定用例的输入输出,那么,编写测试代码形成的干扰就会降低到最低限度,更好地发挥程序员宝贵的灵感、创意和思路,也不会引起程序员本能的抵制情绪。
结束语 以上观点欢迎拍砖。笔者对测试先行理念非常赞同,但认为不能太教条和形式主义,而是要结合实际,更加人性化。敏捷开发强调“拥抱变化”,“实事求是”,既然如此,作为敏捷开发的基础和核心,单元测试方法是不是也应该结合实际、与时俱进?说实话,笔者对一些敏捷大师过份维护敏捷方法的纯粹性,排斥改进和自动化的态度很不以为然,这种圈地自闭的态度,本身就是违背敏捷精神的。