2001年2月《敏捷宣言》签定,宣告了敏捷开发运动的开始。敏捷的开发方式引起越来越多开发者的关注,世界上出现了各种各样的敏捷软件开发方法, SCRUM,Crystal,特征驱动开发(Feature Driven Development,FDD),自适应软件开发(Adaptive Software Development,ADP)以及极限编程(eXtreme Programming,XP) 等等。其中极限编程是敏捷方法中最著名的一个,由Kent Beck在1998年提出,由一系列简单却互相依赖的实践组成。其中对如何开发代码提出了的要求:结对开发+TDD。
关于TDD,可能一些富有经验的程序员会说这个看起来很怪而且似乎没有必要。这些程序员很聪明,他们具有许多经验,他们说不需要先编写测试,因为他们知道自己在做什么。
那么TDD真的没有必要么? 答案否定的, 个人觉得他们没有真正的了解过TDD,下面由我来给大家介绍一下TDD。
什么是TDD
TDD,Test-Driven Development,测试驱动开发。是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP,同样可以适用于其他开发方法和过程。
TDD的重要目的不仅仅是测试软件,测试工作保证代码质量仅仅是其中一部分,更重要的是在开发过程中帮助客户和程序员去除模棱两可的需求。TDD首先考虑使用需求(对象、功能、过程、接口等),主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。
所以,TDD具有2大特性:
为什么要使用TDD
如何使用TDD
我们知道XP涉及两种测试:程序员测试和客户测试。
TDD(也称为测试为先编程)通常指第一种测试,至少我使用这个术语时是这样。测试驱动的编程是让程序员测试决定你所编写的代码。这意味着我们必须在编写代码之前进行测试。测试指出我们需要编写的代码,从而也决定了我们要编写的代码。我们只需编写足够通过测试的代码即可,Uncle Bob在《Clean Code》一书中强调代码恰好通过测试即可。XP中也有类似的要求:如果不进行程序员测试,则你不知道要编写什么代码,所以你不会去编写任何代码。
每当新增一个功能时,我们执行以下流程:
1) 明确当前要完成的功能。可以录成一个TODO列表。
2) 快速完成针对此功能的测试用例编写。
3) 测试代码编译不通过。
4) 编写对应的功能代码。
5) 测试通过。
6) 对代码进行重构,并保证测试通过。
7) 循环完成所有功能的开发。
在用TDD编写代码时我们要遵守TDD三定律。这保证每个循环大概在30秒钟,测试代码和生成代码一起编写,测试只比生成代码早写几秒钟。
TDD三定律
测试范围、粒度
对哪些功能进行测试?会不会太繁琐?什么时候可以停止测试?这些问题比较常见。按 Kent Beck 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。感到疲劳就应该停下来休息一下。感觉没有必要更详细的测试,就停止本轮测试。
测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据你的经验,功能复杂、核心功能的代码就应该编写更全面、细致的测试用例,否则测试流程即可。
测试范围没有静态的标准,同时也应该可以随着时间改变。对于开始没有编写足够的测试的功能代码,随着bug的出现,根据bug补齐相关的测试用例即可。
小步前进的原则,要求我们对大的功能块测试时,应该先分拆成更小的功能块进行测试,比如一个类A使用了类B、C,就应该编写到A使用B、C功能的测试代码前,完成对B、C的测试和开发。那么是不是每个小类或者小函数都应该测试哪?我认为没有必要。你应该运用你的经验,对那些可能出问题的地方重点测试,感觉不可能出问题的地方就等它真正出问题的时候再补测试吧。
怎么编写测试用例
测试用例的编写就用上了传统的测试技术。
TDD原则
不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。
开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。
需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。
这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。
测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。
功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。
无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。关于重构,我会另撰文详细分析。
软件开发是个复杂性非常高的工作,开发过程中要考虑很多东西,包括代码的正确性、可扩展性、性能等等,很多问题都是因为复杂性太大导致的。极限编程提出了一个非常好的思路就是小步前进。把所有的规模大、复杂性高的工作,分解成小的任务来完成。对于一个类来说,一个功能一个功能的完成,如果太困难就再分解。每个功能的完成就走测试代码-功能代码-测试-重构的循环。通过分解降低整个系统开发的复杂性。这样的效果非常明显。几个小的功能代码完成后,大的功能代码几乎是不用调试就可以通过。一个个类方法的实现,很快就看到整个类很快就完成啦。本来感觉很多特性需要增加,很快就会看到没有几个啦。你甚至会为这个速度感到震惊。
TDD不仅仅是测试
TDD是设计方法
TDD的基本思路就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作,而是把需求分析,设计,质量控制量化的过程。
需求向来就是软件开发过程中感觉最不好明确描述、易变的东西。这里说的需求不只是指用户的需求,还包括对代码的使用需求。很多开发人员最害怕的就是后期还要修改某个类或者函数的接口进行修改或者扩展,为什么会发生这样的事情就是因为这部分代码的使用需求没有很好的描述。测试驱动开发就是通过编写测试用例,先考虑代码的使用需求(包括功能、过程、接口等),而且这个描述是无二义的,可执行验证的。
通过编写这部分代码的测试用例,对其功能的分解、使用过程、接口都进行了设计。而且这种从使用角度对代码的设计通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。因此测试驱动开发也是一种代码设计的过程。
TDD是学习方式
开发人员通常对编写文档非常厌烦,但要使用、理解别人的代码时通常又希望能有文档进行指导。而测试驱动开发过程中产生的测试用例代码就是对代码的最好的解释。 当你不知道如何使用该类时,可以查看测试,看一个非常具体的示例。这是学习的很好方式。
TDD是信心来源
快乐工作的基础就是对自己有信心,对自己的工作成果有信心。当前很多开发人员却经常在担心:“代码是否正确?”“辛苦编写的代码还有没有严重bug?”“修改的新代码对其他部分有没有影响?”。这种担心甚至导致某些代码应该修改却不敢修改的地步。测试驱动开发提供的测试集就可以作为你信心的来源。
当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位bug。而迅速发现、定位bug是很多开发人员的梦想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位bug提供了条件。
程序员可以按一个按钮就运行这些测试。几秒种之后,就可以知道代码是否按我们告诉它应该怎样的方式来运行。这种可回归的工具是很有价值的。团队中的任何人以在任何时候更改代码,甚至在代码发布的前一天也可以更改,因为如果有任何问题,测试会立即告诉他们。作为一名程序员, 这给予了我信心 ― 比大多数程序员具有更大的信心。