InfoQ:请介绍你自己,以及TDD的实践经验。
熊节:ThoughtWorks公司总监咨询师,曾参与《重构:改善既有代码的设计(中文版)》、《J2EE核心模式(原书第2版)》、《Contributing to Eclipse中文版》等图书的翻译。目前正在从事Ruby on Rails的项目,并致力于敏捷方法与思想的推广。
鲍央舟:OutSofting的敏捷咨询师。在从事咨询工作之前,从事软件工程师的工作。接触、使用过一些TDD的实践,但是由于大环境的原因,没有深入使用。在从事咨询工作之后,和一些有经验的TDD高手做过Pair,也对TDD有了更深入的理解。
滕正宇:独立敏捷教练,InfoQ敏捷社区编辑,国内唯一的认证Scrum教练(Certified ScrumCoach)。从2005年接触到持续集成并开始实践,接下来开始接触并实践TDD以及Scrum等敏捷方法。
田乐:无限讯奇高级开发工程师,工作有7年,是一个前端后端都做的程序员。从07年开始使用TDD。
段念:Google中国高级测试经理,10来年软件开发与测试经验,主要工作方向在软件测试。在某些项目中实践过TDD,ATDD,在推动开发工程师的开发测试过程中对TDD有一些思考。
InfoQ:TDD跟Test是什么关系呢?TDD的T就是Unit Test吗?
熊节:可以是。也可以不是。关键在于,是不是都无所谓。我前两个星期就在干一个用Cucumber+Selenium直接写spec来驱动开发的项目,照样干得很清爽。关键在于,TDD的T,是什么测试都无所谓。它就是设计。或者如果要把词汇定义得再准确点的话,你脑袋里的那个东西是“设计”,你在写生产代码之前写下来的那段东西是“设计的展现形式”,只不过它恰好是一个可以执行的JUnit或者whatever测试用例。
那么接下来应该问的是:什么是合理的设计的展现形式? 答案是,它应该是无二义的、可执行的设计的验收条件。因此你不仅可以说“我有一个设计”,并且可以说“我把设计记录下来了”,并且可以说“我写的代码是符合设计的”。而这后面两件事(尤其是最后一件事),我没有看到其他任何一种设计方法可以有效地保障。
所以“关注测试而不是设计”这种说法,它不是对不对的问题,它是可笑的。当你说一种设计方法不关注设计,你到底是在说什么呢?或者你不认为它是一种设计方法?那么当你说你使用了一种设计方法但你并不认为它是一种设计方法,你到底是在说什么呢?
鲍央舟:TDD更是一种工作方式,编码观念,而Test是这种观念中的一部分实践。具体说来,TDD的观念是先明确下一步要做的一小样东西,然后恰到好处的实现要做的东西,最后审核所做的质量,以此循环。Test是明确下一步东西后的产出,对实现的正确引导,也是审核将来代码质量的一个工具。按术语来说,TDD的T确实是unit test。Unit test是跟代码质量,编码思路密切相关的。也有ATDD,BDD之类的,与代码的外部功能相关。
滕正宇:TDD跟测试的关系:
- 测试是TDD的必然结果。如果团队一直在实践TDD,所有的代码都会有相应的测试,所有的测试其实就是整个系统的脚手架。
- TDD方式的开发是从写测试开始的。
- 使用TDD时,功能开发总是实现沟通结束条件,也就是在何种情况下,可以认为功能完成,这个结束条件是以测试体现的。
- 实践TDD时,写代码只有两种目的:1. 让一个失败的测试通过。2. 在不添加新功能(也就是不需要添加新的测试)的前提下,让代码、结构或者测试更加清晰、整洁、易懂。
狭义上TDD的测试指的是单元测试,但是随着敏捷开发方法的发展,TDD又逐渐延伸发展出了ATDD(AcceptanceTest Driven Development)和BDD(Behavior Driven Development)等。每种方法关注于不同的问题。在这里我用Google的敏捷和TDD教练Misko Hevery基于Mike Cohn的测试金字塔延伸出来的金字塔来说明敏捷开发中各种测试的关系。
但是这些不同的测试之间也没有明确的界限,每个人有不同的理解或者实现方式,很多人也会先从简单的单元测试着手,然后逐步把单元测试重构组合成集成测试用例。
田乐:TDD是从Test开始的,驱动开发过程的动力是失败的测试,Test是驱动设计的工具。TDD肯定需要有Test,不过Test大部分的时候都不是为了TDD而写的。TDD的Test不是Unit Test,它可以是Acceptance test(ATDD)、Functional test等等。一般来说TDD可以穿透测试的几个水平分类。Test Category理论我记得以前InfoQ有写。单元测试对应的是集成测试。单元测试一般指对函数级别或者OO环境中的类级别的测试,特点就是没有外部依赖。集成测试是测试组件之间协作的测试,一般都会验证和用户需求有关的一些行为是否可以完成,也可以验证设计的组件协作是否可以完成。大型的系统还会把最顶端的集成测试叫做系统测试。AT或者UAT是从系统的外部行为验证软件的功能是否可以完成,它们在这种分层维度里面应该算系统测试,只是它要求这种系统测试是黑盒的。
功能测试是对应非功能测试的,就是是指验收的场景是否和Use Case有关。还有回归测试,它一般是用来验证曾经发现的缺陷的。
TDD中的测试可以是上面所有分类中的任何一种。
段念:TDD并不是石头里蹦出来的孙悟空,DBC(Design By Contract)可以看作是TDD的前身。在DBC的观点里,设计应该以规约(Contract)的形势体现,规约定义了被开发对象的行为。延续这个观点到TDD,很容易就能理解,TDD中的T,在表现形式上是“测试”,但其实,它更应该被理解为“对被实现对象”的行为限定,也就是DBC中的规约。“测试”只是用来体现规约的形式。单元测试通常被定义为“对应用最小组成单位的测试”,它的测试对象通常是函数或是类,在对类的设计和实现应用TDD时,为类建立的测试通常与类的单元测试相当类似,因此TDD中的T往往被误认为是单元测试本身。实际上,这两者还是有显著的区别的。首先,TDD中的T描述的是规约,是设计的一部分;其次,TDD中的T并不明确要求T对实现代码的覆盖率;第三,TDD的T的侧重点是“描述被实现对象应该具有的行为”,而不仅仅是“验证该类的行为是否正确”。当然,TDD中的T在形式上是测试,在重构中也可以作为被实现对象的行为验证框架。
单元测试、集成测试、系统测试、用户验收测试是基于传统软件开发过程的划分,在传统软件开发观点中,这几类测试不仅意味这测试对象的不同,同样也以为着不同的测试在开发周期中处于不同的位置。但在敏捷开发中,如果继续使用这几个名词,最多也只能保留它们在测试对象方面的含义。对于TDD来说(ATDD和BDD可以认为是TDD的变体),在不同的测试类别中都可以应用之,唯一的区别在于T面向的对象不同。
InfoQ:你认为实施TDD需要怎样的前提条件?TDD难在哪儿?
熊节:要使用一种设计方法,你就必须(1)会做设计;(2)做设计。它难在有些项目不做设计,有些人不会做设计。
鲍央舟:实施TDD的前提是对TDD实践背后的观念有所理解,也需要有经验者的指导。TDD难在习惯和观念的转变。以前的工作方式已经在大脑和肌肉中固定下来,无法短期更改。另外,在短期可能呈现开发速度更慢的现象,需要管理层也对TDD以及质量有所理解,才能给予正确的支持。
滕正宇:前提条件很简单,就是了解这种工作方式(可以通过读书,可以通过跟教练一起结对等),然后去坚持。TDD很难:
- 关键是人的因素,人的个人修养。类似于健身,人人都知道经常锻炼的好处,但是真正没有几个人去坚持,这需要很高的纪律性。
- 要了解的东西太多,需要不断学习,如果不了解如何做好的设计,怎样去解耦等等,否则TDD会做得很痛苦。多数人却比较懒散。
- 大家往往认为老的系统代码太烂,耦合度太高,无从下手。
田乐:前提条件就是需求明确,知道需求的边界,了解如何可以验收。另一方面TDD要经过一些练习。我觉得TDD的一个难点就是把它当作一个推动设计的工具,而不是停留在保证质量(如检查边界条件)这个层面。另外,由于TDD一般是从最外面的抽象开始的,所以我个人觉得TDD最开始的抽象模型选择也是一个难点。
段念:实施TDD是对开发者行为的比较大的改变,在我的经历中,遇到的主要难点应该是改变既有的开发工程师的开发习惯吧。TDD技术本身没有什么特别的要求,任何组织都可以直接应用。
InfoQ:TDD之于需求、设计、代码质量是怎样的关系和影响?
鲍央舟:对于需求来说,TDD更能引导开发人员做出真正符合需求的东西,不会过渡开发。对于设计来说,TDD的实践能帮你清理思路,但不能教会你做好的设计。对于质量来说,TDD保证所有的代码都有测试覆盖,肯定能提高质量。
滕正宇:需求方面根据2002年Standish Group的报告,我们软件系统中有65%的功能是客户从来不用或者很少用到的。传统意义上大家认为敏捷开发应该让我们的团队开发得更快,生产率更高,这其实是很大的误解。与提高效率相比,使用Scrum,TDD能够帮我们从整个需求中确定真正有用的那35%,而且往往这35%的功能往往实现起来并不是那么困难。因为做得少,所以做得更快。TDD与设计、代码质量方面,我想引用Keith Braithwaite在XPDay的分享话题“Measure for Measure”。他分析了很多的开源项目,发现有趣的现象,有自动化测试的那些项目,质量要好于没有TDD的项目(参见下图,所有有自动化测试的项目斜率(Slope)都是>2 的,想要了解斜率的具体含义,可以去听Keith的话题分享)
田乐:TDD会让你减少无用功,因为它迫使你从需求验收的角度入手,你必须在进入细节之前找到这个需求的边界,找到那些验收条件。TDD会锻炼自顶向下的的抽象分解能力,这对仅习惯从细节入手的程序员来说很有帮助。TDD可以提高你的代码的可测试性,它的节奏还可以帮助你做到简单设计。还有大家常说的,TDD有助于提高代码的内部质量。
段念:ATDD可以向上直接追朔到需求,使用ATDD方式,可以避免功能镀金,这是TDD技术带来的一个大的好处。在设计方面,TDD并不追求在最开始的时候得到一个完备的设计,而是遵循“够用的设计”的原则,保证开发者可以在短时间内得到可用的设计与实现。“够用的设计”是一个在敏捷环境下非常有效的原则,当然,也有些人反对这个原则,认为随意的设计无法随着应用的复杂性增加则很好的适应。在我看来,由于需求本身的不确定性,很难期望在一开始的时候就能给出保证能满足将来需求的设计,既然这样,不如遵循“够用的设计”的原则,通过重构等方式不断修正和优化设计。TDD对代码质量本身没有明确的关注,但如果开发者自觉地不断应用重构技术消除代码中的bad smell,在组织级别设计强制的code review,以及对单元测试覆盖率给出明确要求,则可以在很大程度上让代码质量保持在高水平上。
InfoQ: 你认为实施TDD容易犯的错误是什么?TDD的不足在哪些方面?
熊节:错误?陈皓同学已经向我们展示了。当你使用一把锤子时,你能犯的最大的错误就是尝试用它把钉子撬出来而不是砸进去。
不足?当你不知道该怎么用最理想的方式来描述你的设计,好吧,不管是因为什么原因,你当然只好退而求其次。你尽可以把设计的复杂和设计能力的欠缺归咎于你用的锤子。反正TDD又不会说话。不过,当你甚至不能为你的设计写出一个测试,你究竟打算用什么方式向别人讲述这个设计呢?或者也跟着退而求其次,反正我已经有设计了你就别管这个设计是什么反正我做出什么你就用什么吧──没错,很多人一直是这样工作的。
鲍央舟:过度编码!写完测试后,代码不止使新测试通过,还实现了很多别的东西。不足只是真正掌握比较难而已。
滕正宇:容易犯的错误:
- TDD的名字取得不好,大家往往产生误解,认为只要先写测试,再写代码就是TDD。在很多号称使用TDD的公司,这些测试甚至是测试人员写的(这不就是瀑布里面的V模型么?)
- 不重构,只要让测试用例通过就结束。对于一个TDD的实践者来说,他往往花很少的时间去实现功能,让用例通过。他会花绝大部分时间去重构,去让代码变得更加容易理解,设计更加清晰。
不足之处:
- 太需要实践者的坚持,“上士闻道,勤而行之,中士闻道,若即若离,下士闻道,大笑之,不笑不足以为道。”但是往往只有少数人会真正踏踏实实去实践。有些人三天打鱼两天上网,另外一些人会去嘲笑这种做法。
- 很难去向公司上层以及项目经理去证明其价值。
田乐:我觉得TDD最容易出现的就是节奏问题,由于有的时候coding的非常尽兴,就没有遵循红绿红绿小步前进(baby step)的节奏,而是在没有失败的测试的时候就洋洋洒洒写下去。那样的做法其实就不完全是TDD了。TDD的不足是它不是万能的,不应该是强制的。不是所有的任务都需要TDD,那些临时的可抛弃的代码(如技术可行性试验)不需要TDD。强制的TDD和强制的单元测试一样,因为设计优略不容易量化,TDD也不能用TestCase的数量去量化,没法量化的实践不应该是强制的,否则会流于形式。TDD无法量化也是它被大规模推广的时候的一个不足。
段念:把TDD等同于单元测试,认为TDD只是“提前写单元测试”这种想法应该是很多不太了解TDD的人容易犯的错误吧。如果把TDD放到敏捷开发的大背景下,我倒不觉得TDD有什么明显的不足,但如果单独考量TDD在企业中的实践,TDD技术本身不关注代码的质量应该是一个明显的问题。应用TDD的企业通常需要采用持续的Code Review和Refactory方法保证通过TDD产生的代码的质量。
InfoQ:一般开发者需要多久能掌握TDD呢?请向读者推荐一下TDD的学习资料吧。
熊节:到他们掌握该怎么做设计时──which never happens to most people,请参阅《程序员的思维修炼》。 至于学习资料么,问豆瓣都比问我好。这事情也很悖论:能学会的人,读这些文字的价值约等于0,因为他只需要豆瓣搜索1分钟+阅读1小时──这是他anyway会做的事──就能得到同样的信息;读了这些文字觉得很有用的人,有鉴于Kent Beck那本书出版已经7年了,基本上,他已经没希望了。
鲍央舟:半年!网上一搜可以一大把资料,不过我觉得TDD光看是没用的,一定要和有经验的人pair!
滕正宇:多久才能掌握TDD呢?这不是一个“敏捷”的问题,因为每个人的学习能力,和对学习的投入程度是不一样的,因此学习的“速度”也不一样,因此很难说需要多场时间。在这里我采用敏捷计划与估计的做法,我们先确定一下范围,然后每个读者根据自己的学习速度,自然就可以算出需要多场时间。做好TDD需要了解很多的技巧、工具、原则,我想用Ron Jeffries的“敏捷技能的七个支柱”来说明需要掌握的东西。在这七个支柱中
- 需要掌握“技术卓越”(Technical Excellence)
- “业务价值”中的“用户故事”及“递增迭带式发布”
- “自信”中的“持续集成”
- “产品”中的“易学易用”
所有这些背后都隐含着很多的学习要点,这些要点需要通过读很多书、资料、与专家交流才能掌握。
TDD的学习资料
书:
- Kent Beck的“测试驱动开发”
- Martin Fowler的“重构”
- Bob大叔的“敏捷软件开发原则、模式、实践”和“代码整洁之道”
- Michael Feathers的“修改代码的艺术”
- Gerard Meszaros的“XUnit模式”
- Roy Osherove的“The Art of Test Driven Development”
- Steve Freeman的“Growing Object Oriented Software Guided by Tests”
- Eric Evans的“领域驱动设计”等。
视频
- 到Google去查Kata
- James Shore的“Let’s Play TDD”系列
田乐:不知道多久可以掌握。学习资料的话Kent Beck的TDD就很好,还有我觉得TDD很适合与设计的方法论一起看,如领域模型驱动设计等(《测试驱动的面向对象软件开发》就结合了这两个知识)。
段念:我们组织中有一些用于帮助开发工程师熟悉TDD的program,根据我的了解,一般的开发工程师可以在1-2周内掌握TDD工作方式,但一般需要更长时间来达到对TDD的熟练掌握和灵活应用。