测试驱动开发(TDD)的全称是Test-Driven Development。上一篇文章中,介绍了Kent Beck,他是极限编程的创始人,测试驱动开发是在极限编程中引入的一种编程实践和技术,是敏捷软件开发的关键实践之一。测试驱动开发是先编写测试案例,然后再实现代码。
Kent和Erich Gamma在1997年10月搭乘同一趟航班飞往美国亚特兰,参加一年一度的OOPSLA(Object Oriented Programming Systems Languages &Applications)会议。OOPSLA会议是美国计算机协会(ACM)举办的一个年度性会议,第一届是1986年在美国波特兰市举办,最后一期是2010年,后来成为了SPLASH (Systems, Programming, Languages, and Applications: Software for Humanity)的一部分,是一个IT行业牛人云集的会议,会议的主题主要是编程界的前沿技术,具有一定的学术性,比如1997年有一个关于Java(Type Parameterization)泛型的话题,直到2004年的JDK5发布,才正式的支持泛型。
Kent和Erich还是是Eclipse的主要架构设计者,Erich 2011年加入微软,负责Visual Studio Code的开发。Erich最著名的书是《设计模式》,就是我们通常说的23种经典设计模式的作者之一。在飞机上,两个人一拍即合,完成了最初的JUnit版本,是仿照SUnit的Java版本,SUnit是Smalltalk编程语言的测试框架,Junit框架至今已经进化到第五代了,生态圈也非常的完整。
Kent 2011年加入Facebook,担任软件技术教练,负责公司工程师文化的落地和推广,帮助新员工掌握并使用相关的技能。通过敏捷和自动化测试的实施,Facebook可以做到每周甚至每日发布,而且大多数团队没有专职的QA人员。Kent 2019年加入了一家创业型SaaS软件公司Gusto担任Fellow,做着赋能的事情,该公司是人力资源领域的SaaS独角兽,员工1400人,估值约38亿美金。
近些年,有人在TDD 的基础上提出了ATDD(Acceptance Test Driven Development)的概念,包括 BDD(Behavior Driven Development)和 Consumer-Driven Contracts Development 等编程实践和技术,把TDD分为狭义的和广义的,狭义的 TDD是指UTDD(Unit Test Driven Development),而广义的 TDD 是指 ATDD。本文中所指的TDD是指的UTDD,即单元测试驱动开发。
测试驱动有三层含义:
第一层是驱动设计,测试代码相当于是实现代码的客户端,编写测试案例的时候,会迫使开发人员去思考,代码的接口和方法应该如何设计,如何组织代码,如何被客户端调用等;当然这里的驱动设计是值得商榷的,设计形式是多种的,比如UML辅助,也可以采用领域驱动设计(DDD)的方法。
第二层是驱动代码质量,测试案例来源需求,单元测试通过,就可以证明在测试案例覆盖的范围,代码是经过测试的,从而做到测试和BUG修复的循环变得最小,快速获得反馈提升效率,同时代码的质量也会有保障,未来改动代码的时候,运行单元测试,可以及时发现问题,起到防止代码被改坏的作用;
第三层是驱动重构和改进设计,因为有单元测试的保障,开发人员在重构代码之后就不用担心改出问题。
TDD的编码步骤
TDD 的基本流程是:红,绿,重构。
- 根据需求编写一个测试用例
- 红:运行测试,失败
- 实现代码刚好能让测试通过
- 绿:运行测试,成功,失败进入上一步
- 重构:识别坏味道,用重构手法修改代码
- 绿:运行测试,成功,失败回到上一步
是否采用了TDD的开发方式就不用Debug了呢?
答案是否定的,开发过程中可能遇到测试失败,但是也无法一下看出代码问题的情况,还是需要使用Debug模式调试代码,查看运行是的变量和代码执行路径。
对于TDD,也有很多常见的观点:
- 业务需求变化太快,测试代码改来改去浪费时间;
- 开发时间紧,项目发布都是倒排的计划,根本没时间写TDD代码;
- 不知道怎么写单元测试,Debug就很好用;
- 代码从来不进行重构;
- 使用Mock和Stub技术,UT没有办法测试集成后的功能,业务价值不大;
对于TDD一直存在争议,牛人也是褒贬不一,Kent Beck、Martin Fowler、David Heinemeier Hansson(RoR之父)在2014年有过一场关于TDD的讨论,主题是“TDD已经死了吗?”,有兴趣的可以听一下对话。Is TDD Dead
David他之后又写了“ TDD 已死 - 测试永生”和“测试引起的设计伤害”这两篇文章来对这一主题进行了扩展。
我个人的观点就是,既然测试很重要,那测试是必须的,至于是先测试还是后测试,在开发过程中一定交织在一起的,不用过于纠结是不是TDD。以下是几个实践的建议:
1、不写单元测试,不允许提交代码
2、提交代码前需要本地运行单元测试确保通过
3、持续集成平台需要持续运行单元测试
4、不过度追求100%测试覆盖率
我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。
我们的探寻还尚未结束,TDD 只是我们在这一历程中所寻找到的其中一种,还存在其他方式供我们去继续寻找。
以下内容是经典的TDD的练习题目,FizzBuzz游戏,大家感兴趣可以练习并实践一下,参考视频。
为什么别人写代码那么快?这篇文章是熊节(重构一书译者)举办的程序员练功房培训班里面的作业视频,最快的1分15秒完成了编程,大部分能够在5-10分钟内完成编程。
我自己录制的视频,采用了传统的编码方式,用时6分23秒,差距还是有的。
测试驱动开发示例
想象一下你上小学五年级,在课程结束前的五分钟,你的数学老师说要带着大家玩一个小游戏。他将依次指着每个学生,并要求他们从一开始依次报数。第⼀个被指着的学⽣说“1”,第⼆个被指着的学⽣说“2”,如果数字可被三整除,则不能说数字,要说“Fizz”,如果数字被五可整除,则说“Buzz”。
游戏开始了,⽼师的⼿指向⼀个个同学,他们开⼼地喊着:“1!”,“2!”,“Fizz!”,“4!”,“Buzz!”……终于,⽼师指向了你,时间仿佛静⽌,你的嘴发⼲,你的掌⼼在出汗,你仔细计算,然后终于喊出“Fizz!”。运⽓不错,你躲过了⼀劫,游戏继续进⾏。
为了避免在⾃⼰这⼉尴尬,我们想了⼀个作弊的法⼦:最好能提前把整个列表打印出来,这样就知道到我这⼉的时候该说什么了。全班有33个同学,游戏可能会玩2到3轮。
任务:写⼀个程序,打印出从1到100的数字,将其中3的倍数替换成“Fizz”,5的倍数替换成“Buzz”。既能被3整除、⼜能被5整除的数则替换成FizzBuzz。
样本输出:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
...
第二阶段,新需求:
- 如果⼀个数能被3整除,或者包含数字3,那么这个数就是“Fizz”
- 如果⼀个数能被5整除,或者包含数字5,那么这个数就是“Buzz”