虚拟座谈会:TDD 有多美?

阅读数:13835 2011 年 2 月 23 日

话题:Java敏捷.NETRuby语言 & 开发架构文化 & 方法

【编者按】最近酷壳的一篇有关 TDD 的文章引起了广泛关注,对于 TDD 一些人有自己不同的见解,为此 InfoQ 中文站特地邀请了 InfoQ 内外的敏捷专家特别是有丰富 TDD 实践经验的人,就 TDD 为 InfoQ 的读者分享他们自己的经验和体会。

InfoQ:请介绍你自己,以及 TDD 的实践经验。

熊节:ThoughtWorks 中国公司首席咨询师,曾参与《重构:改善既有代码的设计(中文版)》、《J2EE 核心模式(原书第 2 版)》、《Contributing to Eclipse 中文版》等图书的翻译。目前正在从事 Ruby on Rails 的项目,并致力于敏捷方法与思想的推广。熊节从 2003 年开始实践 TDD,从此以后使用此方法编写每一段交付使用的代码,直至今天。
鲍央舟:OutSofting 的敏捷咨询师。在从事咨询工作之前,从事软件工程师的工作。接触、使用过一些 TDD 的实践,但是由于大环境的原因,没有深入使用。在从事咨询工作之后,和一些有经验的 TDD 高手做过 Pair,也对 TDD 有了更深入的理解。
滕振宇:独立敏捷教练,InfoQ 敏捷社区编辑,国内唯一的认证 Scrum 教练 (Certified Scrum Coach)。从 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(Acceptance Test Driven Development)和 BDD(Behavior Driven Development)等。每种方法关注于不同的问题。在这里我用 Google 的敏捷和 TDD 教练 Misko Hevery 基于 Mike Cohn 的测试金字塔延伸出来的金字塔来说明敏捷开发中各种测试的关系。

  • 基于 UI 的测试,这一部分不可能做测试驱动,因为 UI 总是在最后完成的,不可能提前。
  • 功能测试,这一部分是关注于系统的业务逻辑,通常是用 ATDD 和 BDD,是集成测试。这一部分保证系统做正确的事情 (Do Right Things)。在迭带初期阶段,讨论用户故事的结束条件,用例子来体现,这些例子就形成了自动化的功能测试。
  • 单元测试,这一部分保证系统的每个单元(类和方法级别)的逻辑正确,更关注于正确地做事情 (Do Things Right)。
但是这些不同的测试之间也没有明确的界限,每个人有不同的理解或者实现方式,很多人也会先从简单的单元测试着手,然后逐步把单元测试重构组合成集成测试用例。
田乐: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 的熟练掌握和灵活应用。