什么是TDD

TDD的核心思想是先写测试用例,然后通过测试来推动整个开发的进行,包括接口的设计,代码的编写和重构。

Alt text

整个TDD的过程就是红绿蓝三色循环迭代:

Alt text

为什么要推行TDD

首先一个显而易见的好处就是编码阶段,通过单元测试验证我们的代码,可以方便的进行本地自测,提高代码的质量。《重构》中Matin Flower就说:一套测试就是一个强大的bug侦测机,能够大大缩减查找bug所需的时间。

但是TDD最大的好处还在于它不只是覆盖编码阶段,还对设计阶段产生重大影响。因为要提前写测试用例,意味着我们需要从接口调用方的角度使用我们定义的接口,这种角色的换位会让我们更好的设计出面向用户的友好接口。并且,为了能够单元测试该接口,在接口设计上还会考虑到接口的可测性,逼迫我们设计出可测性良好的接口。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。

最后,TDD在重构阶段特别的重要。Matin Flower在他的《重构》书中强调:重构的第一步,就是检查自己是否有一套可靠的测试集。可见TDD对重构有多重要。它给重构带来极大的信心, 特别是面对一个庞大而复杂的遗留系统。

TDD鼓励最小化的修改,小步前进。有了单元测试,意味着反馈非常的快,当我们做了一小部的修改后就立即跑一下单元测试验证一下是否成功,如果成功就继续下一个功能重构,通过每一个小的重构保证最后大的重构的完成,可以极大的提高重构的效率和重构的质量。相对于一次修改200行代码再跑不过定位问题,显然前者要简单的多。

TDD相关工具

TDD已经有很多成熟的工具,像Java有 JUnit/TestNG,Mockito/EasyMock 等,C++有 gtest,gmock,其他语言也有类似的框架,这里就不展开介绍了,网上有很多资料。

TDD的原则

好的单元测试应该遵循FIRST原则AIR原则。这两个原则有一些是重合的。

FIRST原则

  • F-FAST(快速原则):单元测试应该是可以快速运行的,在各种测试方法中,单元测试的运行速度是最快的,通常应该在几分钟内运行完毕
  • I-Independent(独立原则):单元测试应该是可以独立运行的,单元测试用例互相无强依赖,无对外部资源的强依赖
  • R-Repeatable(可重复原则):单元测试应该可以稳定重复的运行,并且每次运行的结果都是相同的
  • S-Self Validating(自我验证原则):单元测试应该是用例自动进行验证的,不能依赖人工验证
  • T-Timely(及时原则):单元测试必须及时的进行编写,更新和维护,以保证用例可以随着业务代码的变化动态的保障质量

AIR原则

  • A-Automatic(自动化原则):单元测试应该是自动运行,自动校验,自动给出结果
  • I-Independent(独立性原则):单元测试应该是独立运行,互相之间无依赖,对外部资源无依赖,多次运行之间无依赖
  • R-Repeatable(可重复原则):也叫做可幂性,意思是单元测试是可重复运行的,每次的结果都稳定可靠。为了保证单元测试的可幂性,需要引入测试数据构造和mock机制。

实践例子

在我之前的公司,为了推广单元测试以及TDD,我们对所有成员分批次的进行技能培训,覆盖clean code, TDD以及重构。为什么要培训三门课,是因为这些课程是息息相关的,如果只是培训了TDD,在实践的过程中就会碰到各种各样的问题,因为TDD讲究的是红绿蓝转换,在蓝色(重构)阶段就需要用到重构以及clean code的知识,同时,在一开始进行代码编写的时候也是需要具有基本的职业素养。

同时,经过了TDD实践的项目,在按质量交付以及后续维护等层面都有大幅的提高。在原来,开发一个产品需要经过严苛的测试,经常会有反复的情况,测试的时间会比开发的时间还要长。一般一个产品发布周期有两年。经过TDD后,产品发布周期大幅缩短,在支持不同的客户的时候(也会对外支持给第三方定制),也能很快的发布产品(平均周期为6-9个月)。

一些思考

1、关于接口的可测性

好的接口不仅仅是对用户友好,还应该是对自己友好。其中很大方面表现在可测性上。简单来说,POJO对象可测性最高,所以尽量提供POJO参数接口。

最容易犯的错误就是把框架的对象到处传递,污染业务代码,特别是比较重的框架,比如之前的EJB,还有各种servlet相关web层对象。这会导致可移植性,可测性和可复用性大大变差。例如在我们现在 CloudSoup 根据 swagger YAML 生成的MVC骨架带有 HttpContext 对象,这个对象里面有 HttpServletRequest 和 HttpServletResponse 对象,业务同学在往下调用service的时候又继续把这个对象作为参数往下传递了,这就导致这些接口的可测性非常的差。因为你必须启动整个容器,才能测试。

2、是否要追求百分之一百的单元测试覆盖率?

没有必要追求百分之百的覆盖率,按照大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。那些简单的边缘的代码感觉没有必要,那就可以不写测试。

3、TDD会增加工作量,降低交付速度?

  • 一开始可能会慢一些,特别是一个本地都跑不起来的遗留系统,需要做一些工作让其可以跑单元测试
  • 长期看会平稳收敛,而传统方式是线性甚至指数级增长

Alt text