原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (九)测试驱动开发(TDD)

发布于: 2020 年 05 月 27 日
原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (九)测试驱动开发(TDD)

上一部分我们介绍了单元测试,并列举了单元测试的好处。这一部分我们介绍极限编程的核心实践——测试驱动开发(Test-Driven Test,简称TDD)。从做法来看:

TDD = Test First + Refactor

测试驱动开发 = 测试先行 + 重构

TDD要求先写好单元测试,然后再编写能够通过测试的(可能脏乱差)产品代码,最后再重构产品代码使其更加简洁。

TDD是Kent Beck开创的极限编程(Extreme Programming)这种敏捷方法论的核心实践之一。

1. TDD的目标:Clean code that works

极限编程、JUnit和TDD的创始人Kent Beck在他著名的《Test-Driven Development by Example》一书中,开宗明义地提出:

代码简洁可用(Clean code that works)这句言简意赅的话,正是 TDD 所追求的目标。

这句话说明,对于编码来说,有两个目标: 

  1. Code that works: 代码首先必须可用——单元测试

为了证明代码的正确性和可靠性,我们必须针对代码所要实现意图进行需求分析,定义产品代码的功能以及在各种条件下的预期响应。分析的结果以一组单元测试的形式固定下来,作为产品代码的验收标准。然后就可以去编写实现代码,目的是通过测试,使得产品代码达到“可用(code that works)”状态。

在这个阶段,我们编码的首要目标是通过测试,而不是代码的简洁、优雅、高效与否。

  1. Clean code: 代码应该尽可能简洁——重构

在上一个阶段中,我们的编码目标是以最快的速度通过测试,写下的产品代码可能是脏乱差的,例如可能包含了多层的分支语句,有一些代码重复,一些该使用常量的地方直接写入了字面量,性能不佳,等等,总而言之,代码的内部质量可能不够好。为了达到代码简洁(Clean code)的目标,提高内部质量,我们应该对此前匆匆草就的代码进行重构。

所谓重构,就是“在不改变代码外部可见行为的前提下改善其内部结构”。重构的最大担心是,我们不知道我们对代码内部结构的修改是否破坏了它的外部可见行为,有鉴于此,我们可能在对代码进行修改的时候破坏了软件的功能,甚至可能引进了新的bug,而我们却不知道。

这个时候,我们预先编写的单元测试就发挥了巨大作用了——因为判断是否改变了代码外部可见行为的标准就是测试是否失败。重构之后,我们只需要重新运行一遍单元测试。如果测试仍然通过,就说明重构是成功的,没有破坏任何东西。如果测试失败,就说明重构破坏了代码的契约,我们需要重新重构,直到通过了测试为止。只是此前编写的单元测试为我们的重构设置了安全网。

2. 为什么要测试先行

TDD最引人注目的特征,就是逆转了测试和编码的顺序,则测试先行

为什么要测试先行?下面是一些明显的理由:

  • 人性本懒。“后行”往往等于“没有”。

人性本来就是懒惰的。如果你写完了产品代码,初步运行觉得没问题,你就会觉得再写单元测试是多此一举。再加上“进度压力”等等天然的借口,大多数人就会放弃编写单元测试了。我相信,大多数人不编写单元测试的原因,就是因为没有改变习惯,做到测试先行。

  • 测试先行可以防止写出臃肿复杂难以测试的代码

Martin Fowler说,他见过Kent Beck所编写的软件,每个方法代码行数是3~5行。与此相反,有一个阿里巴巴开源的非常著名的项目,在gitub上面有3万多星,2万多fork,里面多数方法都巨复杂,有的方法有过千行代码,一层套一层的条件分支语句达到15层!这样的代码明显是难于理解、难于维护、难于测试的。遇到这样的方法,任何人都会放弃为它编写单元测试的念头。如果作者真正采用测试先行的实践,就不可能写出这样复杂的代码。

  • 测试用于表达需求、驱动设计,从测试工具变成分析设计工具

这一点才是TDD真正的价值所在。测试先行迫使我们全面分析代码的功能目标以及在各种内外条件下的响应,并以测试的形式固化下来,达到以测试表达需求的结果。从这个角度来看,单元测试成了分析工具。为了实现可测试性,我们的工作单元必须足够简单,代码行数少,局部变量少,分支路径少,没有或很少嵌套分支,依赖项目少,等等等等。为了能够隔离依赖进行测试,我们就要避免在工作单元内创建依赖项,而需要转变成依赖注入,并且使代码依赖于接口而不是实现类。以上种种,在满足可测试性的压力下,程序员被迫优化了设计。从这个角度来看,单元测试又成了设计工具。

TDD的创始人Kent Beck说:

测试驱动开发不是一种测试技术。它是一种分析技术、设计技术,更是一种组织所有开发活动的技术。

因此,有人认为TDD不是Test-Driven Development(测试驱动开发),而是Test-Driven Design(测试驱动设计),强化它的设计工具特征,弱化它的测试工具特征。

3. 如何进行TDD

TDD的基本步骤是所谓的红-绿-重构

在各种IDE中运行单元测试的时候,如果测试失败,会呈现一个红条;测试成功,会呈现一个绿条。

1. 第一步:写一个测试并确保其失败。

测试一定要在产品代码之前编写。在编写测试代码之前被测类及被测方法甚至可以尚未存在!我们可以借助IDE的帮助,由IDE协助我们创建被测对象和方法签名。如果不那么极端,也可以先创建被测产品类及被测方法的方法签名,但绝对不要写方法的实现内容。

要确保这个测试目前执行起来会失败!因为如果在没有代码实现的情况下测试成功了,就说明这是个不相关的测试,而这是不合理的!

2. 第二步:编写产品代码,刚好足够通过测试。

在这一阶段,我们编写代码的唯一目的就是快速通过刚刚编写的单元测试。即使代码写得不好也无所谓。

3. 第三步:重构产品代码,改善其内部质量。

在这一步就要运用各种设计和实现原则,重构代码,达到简洁的目标。

重构完成就再次运行单元测试。如果测试失败,就继续重构,直到最终通过测试。

4. TDD的规则

TDD有3条规则:

  • 除非是为了使一个失败的单元测试通过,否则不允许编写任何产品代码

  • 在一个单元测试中,只允许编写刚好能够导致失败的内容

  • 只允许编写刚好能够使一个失败的单元测试通过的产品代码

第一条规则是为了防止产品代码无的放矢。产品代码这个只能是通过单元测试。如果没有单元测试在前面引导,你写的产品代码就是目标不明确的——它实现了什么?怎么证明它正确实现了?

第二条规则防止你盲目编写测试,或者编写一个太复杂的测试。

第三条规则是为了防止过度设计,或者加入了偏离了需求的内容。

这一章我们就讲到这里,下一章我们讲讲“在项目中准备测试环境”!

发布于: 2020 年 05 月 27 日 阅读数: 11
用户头像

编程道与术

关注

高级架构师,技术顾问,交流公号:编程道与术 2020.04.28 加入

杨宇Yangyu于2020年创立编程道与术,致力于研究领域分析与建模、测试驱动开发、架构设计、自动化构建和持续集成、敏捷开发方法论、微服务、云计算等顶尖技术领域。 了解更多公众号:编程道与术

评论

发布
暂无评论
原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (九)测试驱动开发(TDD)