浅谈测试驱动开发(TDD)

转自:http://www.ibm.com/developerworks/cn/linux/l-tdd/index.html

李群( liqun@nsfocus.com)www.ihere.org

简介:测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量。本文从开发人员使用的角度,介绍了 TDD 优势、原理、过程、原则、测试技术、Tips 等方面。

发布日期:2004 年 11 月 19 日
级别:初级
访问情况9339 次浏览
建议:0(添加评论)

1 star 2 stars 3 stars 4 stars 5 stars 平均分 (共 11 个评分 )

背景

一个高效的软件开发过程对软件开发人员来说是至关重要的,决定着开发是痛苦的挣扎,还是不断进步的喜悦。国人对软件蓝领的不屑,对繁琐冗长的传统开发过程的不耐,使大多数开发人员无所适从。最近兴起的一些软件开发过程相关的技术,提供一些比较高效、实用的软件过程开发方法。其中比较基础、关键的一个技术就是测试驱动开发(Test-Driven Development)。虽然TDD光大于极限编程,但测试驱动开发完全可以单独应用。下面就从开发人员使用的角度进行介绍,使开发人员用最少的代价尽快理解、掌握、应用这种技术。下面分优势,原理,过程,原则,测试技术,Tips等方面进行讨论。

1. 优势

TDD的基本思路就是通过测试来推动整个开发的进行。而测试驱动开发技术并不只是单纯的测试工作。

需求向来就是软件开发过程中感觉最不好明确描述、易变的东西。这里说的需求不只是指用户的需求,还包括对代码的使用需求。很多开发人员最害怕的就是后期还要修改某个类或者函数的接口进行修改或者扩展,为什么会发生这样的事情就是因为这部分代码的使用需求没有很好的描述。测试驱动开发就是通过编写测试用例,先考虑代码的使用需求(包括功能、过程、接口等),而且这个描述是无二义的,可执行验证的。

通过编写这部分代码的测试用例,对其功能的分解、使用过程、接口都进行了设计。而且这种从使用角度对代码的设计通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。因此测试驱动开发也是一种代码设计的过程。

开发人员通常对编写文档非常厌烦,但要使用、理解别人的代码时通常又希望能有文档进行指导。而测试驱动开发过程中产生的测试用例代码就是对代码的最好的解释。

快乐工作的基础就是对自己有信心,对自己的工作成果有信心。当前很多开发人员却经常在担心:“代码是否正确?”“辛苦编写的代码还有没有严重bug?”“修改的新代码对其他部分有没有影响?”。这种担心甚至导致某些代码应该修改却不敢修改的地步。测试驱动开发提供的测试集就可以作为你信心的来源。

当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位bug。而迅速发现、定位bug是很多开发人员的梦想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位bug提供了条件。

我的一段功能非常复杂的代码使用TDD开发完成,真实环境应用中只发现几个bug,而且很快被定位解决。您在应用后,也一定会为那种自信的开发过程,功能不断增加、完善的感觉,迅速发现、定位bug的能力所感染,喜欢这个技术的。

那么是什么样的原理、方法提供上面说的这些好处哪?下面我们就看看TDD的原理。

2. 原理

测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完全部功能的开发。

我们这里把这个技术的应用领域从代码编写扩展到整个开发过程。应该对整个开发过程的各个阶段进行测试驱动,首先思考如何对这个阶段进行测试、验证、考核,并编写相关的测试文档,然后开始下一步工作,最后再验证相关的工作。下图是一个比较流行的测试模型:V测试模型。


【图 V测试模型】
【图 V测试模型】

在开发的各个阶段,包括需求分析、概要设计、详细设计、编码过程中都应该考虑相对应的测试工作,完成相关的测试用例的设计、测试方案、测试计划的编写。这里提到的开发阶段只是举例,根据实际的开发活动进行调整。相关的测试文档也不一定是非常详细复杂的文档,或者什么形式,但应该养成测试驱动的习惯。

关于测试模型,还有X测试模型。这个测试模型,我认为,是对详细阶段和编码阶段进行建模,应该说更详细的描述了详细设计和编码阶段的开发行为。及针对某个功能进行对应的测试驱动开发。


【图 X测试模型】
【图 X测试模型】

基本原理应该说非常简单,那么如何进行实际操作哪,下面对开发过程进行详细的介绍。

3. 过程

软件开发其他阶段的测试驱动开发,根据测试驱动开发的思想完成对应的测试文档即可。下面针对详细设计和编码阶段进行介绍。

测试驱动开发的基本过程如下:

1) 明确当前要完成的功能。可以记录成一个 TODO 列表。

2) 快速完成针对此功能的测试用例编写。

3) 测试代码编译不通过。

4) 编写对应的功能代码。

5) 测试通过。

6) 对代码进行重构,并保证测试通过。

7) 循环完成所有功能的开发。

为了保证整个测试过程比较快捷、方便,通常可以使用测试框架组织所有的测试用例。一个免费的、优秀的测试框架是 Xunit 系列,几乎所有的语言都有对应的测试框架。我曾经写过一篇文章介绍CppUnit的文章(http://www.ibm.com/developerworks/cn/linux/l-cppunit/index.html)。

开发过程中,通常把测试代码和功能代码分开存放,这里提供一个简单的测试框架使用例子,您可以通过它了解测试框架的使用。下面是文件列表。

	project/				项目主目录
	project/test			测试项目主目录
	project/test/testSeq.cpp		测试seq_t 的测试文件,对其他功能文件的测试文件复制后修改即可
	project/test/testSeq.h
	project/test/Makefile			测试项目的 Makefile 
	project/test/main.cpp			测试项目的主文件,不需要修改
	project/main.cpp		           项目的主文件
	project/seq_t.h			功能代码,被测试文件
	project/Makefile		           项目的 Makefile

主要流程基本如此,但要让你的代码很容易的进行测试,全面又不繁琐的进行测试,还是有很多测试原则和技术需要考虑。

4. 原则

测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。

一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。

测试列表。需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。

测试驱动。这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。

先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。

可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。

及时重构。无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。关于重构,我会另撰文详细分析。

小步前进。软件开发是个复杂性非常高的工作,开发过程中要考虑很多东西,包括代码的正确性、可扩展性、性能等等,很多问题都是因为复杂性太大导致的。极限编程提出了一个非常好的思路就是小步前进。把所有的规模大、复杂性高的工作,分解成小的任务来完成。对于一个类来说,一个功能一个功能的完成,如果太困难就再分解。每个功能的完成就走测试代码-功能代码-测试-重构的循环。通过分解降低整个系统开发的复杂性。这样的效果非常明显。几个小的功能代码完成后,大的功能代码几乎是不用调试就可以通过。一个个类方法的实现,很快就看到整个类很快就完成啦。本来感觉很多特性需要增加,很快就会看到没有几个啦。你甚至会为这个速度感到震惊。(我理解,是大幅度减少调试、出错的时间产生的这种速度感)

5. 测试技术

5.1. 测试范围、粒度

对哪些功能进行测试?会不会太繁琐?什么时候可以停止测试?这些问题比较常见。按大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。感到疲劳就应该停下来休息一下。感觉没有必要更详细的测试,就停止本轮测试。

测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据你的经验,功能复杂、核心功能的代码就应该编写更全面、细致的测试用例,否则测试流程即可。

测试范围没有静态的标准,同时也应该可以随着时间改变。对于开始没有编写足够的测试的功能代码,随着bug的出现,根据bug补齐相关的测试用例即可。

小步前进的原则,要求我们对大的功能块测试时,应该先分拆成更小的功能块进行测试,比如一个类A使用了类B、C,就应该编写到A使用B、C功能的测试代码前,完成对B、C的测试和开发。那么是不是每个小类或者小函数都应该测试哪?我认为没有必要。你应该运用你的经验,对那些可能出问题的地方重点测试,感觉不可能出问题的地方就等它真正出问题的时候再补测试吧。

5.2. 怎么编写测试用例

测试用例的编写就用上了传统的测试技术。

  • 操作过程尽量模拟正常使用的过程。
  • 全面的测试用例应该尽量做到分支覆盖,核心代码尽量做到路径覆盖。
  • 测试数据尽量包括:真实数据、边界数据。
  • 测试语句和测试数据应该尽量简单,容易理解。
  • 为了避免对其他代码过多的依赖,可以实现简单的桩函数或桩类(Mock Object)。
  • 如果内部状态非常复杂或者应该判断流程而不是状态,可以通过记录日志字符串的方式进行验证。

6. Tips

很多朋友有疑问,“测试代码的正确性如何保障?是写测试代码还是写测试文档?”这样是不是会陷入“鸡生蛋,蛋生鸡”的循环。其实是不会的。通常测试代码通常是非常简单的,通常围绕着某个情况的正确性判断的几个语句,如果太复杂,就应该继续分解啦。而传统的开发过程通常强调测试文档。但随着开发节奏的加快,用户需求的不断变化,维护高层(需求、概要设计)的测试文档可以,更低层的测试文档的成本的确太大了。而且可实时验证功能正确性的测试代码就是对代码最好的文档。

软件开发过程中,除了遵守上面提到的测试驱动开发的几个原则外,一个需要注意的问题就是,谨防过度设计。编写功能代码时应该关注于完成当前功能点,通过测试,使用最简单、直接的方式来编码。过多的考虑后期的扩展,其他功能的添加,无疑增加了过多的复杂性,容易产生问题。应该等到要添加这些特性时在进行详细的测试驱动开发。到时候,有整套测试用例做基础,通过不断重构很容易添加相关特性。


参考资料

关于作者

李群当前关注于网络安全产品的开发、研究;软件开发过程等方面。您可以通过liqun@nsfocus.com和他联系。

测试驱动开发的半年实战心得

2009-07-27 13:58:40.0 来源:e800技术客
关键词: 测试 驱动 实战 心得

ww6ww

个人页面

登陆交谈

不觉间,采用测试驱动开发(Test Driven Development)半年有余,自从看了Robert Martin的《敏捷软件开发:原则、模式与实践》, 就忍不住想实践一下,亲身体会书中描述的美妙景象。恰逢项目中一个全新功能交由我负责,开发周期也不是十分急迫,就拿这个新功能当回小白鼠,遵循书中的实践方法开始使用测试驱动开发。

  随着开发的不断深入,测试驱动开发的实践渐入佳境,对其认识也从开始时的顶礼膜拜逐渐回归理性,在为其优长欢欣鼓舞的同时,更理解了其不足或者说是不具备的能力。

  测试驱动开发意味着不再是从需求分析与概要设计/详细设计后直接进入到实现代码的编写,而是转而根据需求分析和概要设计进行测试用例的设计与测试代码的编写,通过测试代码硬性规定了实现代码所必须满足的功能需求、容错能力。编写实现代码的唯一目的就是使所有测试用例成功运行,任何测试用例的失败都意味着实现代码存在功能缺陷或者逻辑错误。之后就是不断重复--修改或增加代码再运行所有测试用例检查结果--的迭代过程。

  看上去很简单的一个过程,却与传统的开发格格不入,惯性的力量导致开始时很难从传统过程的思维方式中摆脱出来。在做完需求分析与概要设计,开始设计测试用例与编写测试代码时手足无措,只能摸着石头过河,试着去做去写,然后通过测试驱动开发的迭代过程去观察,去体会,去修改,去适应,然后再重复迭代过程。就这样,渐渐理解了测试驱动开发这种“进化->测试->反馈->再进化->…”迭代循环的自然与强大,从测试中得到的反馈不仅说明实现代码的完备和健全与否,也给人不断进步之感,似乎总是在脚踏实地的前进着。因为在迈开每一步之前,都知道已有的工作经受了测试的检验,就具有相应的信心。即使发现有更好更优秀的设计实现,也可以放心大胆的彻底重构,因为有测试用例和测试代码作监督作守候。

  此外,测试驱动开发也改变了原先开发的视角,不再是直接编写实现代码,而是转而从旁观者和使用者的角度设计测试用例和测试代码,总是能够发现很多原先忽略的因素和条件。

  测试驱动开发的优势在《敏捷软件开发:原则、模式与实践》已有详述,我所感受到的有:

  1. 有助于设计简单清晰而易用的接口。因为总是先有测试代码,才编写实现代码,意味着总是从使用者的角度设计接口,只有简单易用的接口才方便测试时调用,所以我几乎是“被迫”去努力设计简单易用的接口,因为我就是第一个使用者。

  2. 模块切分的足够小但是模块间保持极低的耦合度。为了方便测试,我总是尽力把重复的逻辑剥离出来,单独构建模块进行测试;并且尽量减少模块间的耦合,保持模块相对独立和功能完备。如果模块过大,或者模块间强耦合,写测试用例与代码时就会困难重重,笨重复杂。因为总是先写测试代码,眼前的利益高于一切,将来的实现代码必然要迁就目前测试的需要。于是,我又“不知不觉”设计出小粒度模块,且模块间耦合度低的实现。

  3. 肆无忌惮的重构。因为有测试用例和测试代码作担保,我终于能够从小心翼翼心惊胆战的重构中解脱出来,只要是更好的实现和设计,我都愿意尝试,管它呢,反正多跑几遍测试就知道重构的结果如何了。可以说,测试驱动开发鼓励代码的不断进化,即使测试已经全部通过,也可以通过大胆重构来改进设计与实现。尽管此时是否还需要再重构见仁见智,至少提供了一个可能,我是很喜欢这一点。

  4. 测试代码是“活”的软件文档,它硬性规定了实现代码必须满足的需求,达不到就报错。传统的文本文档比之就苍白无力多了,“应该”,“必须”,这些字眼对程序员有多少约束力?而且测试代码总是能与实现代码保持新鲜同步,传统文档写完后经常被上传服务器束之高阁,很少人问津,随着开发组内人员的变动,往往最后就湮没在服务器的故纸堆之中了。

  测试驱动开发毕竟不是软件银弹,也不存在这样的银弹,它也有力不能及的地方:

  1. 测试驱动开发不可能让人立即具有设计出优美解决方案的能力,或者说是优秀的分析与解决问题的能力。TDD不是Test Driven Design。它只是一个过程,也许可以帮助你发现并帮助你实现优美的解决方案,但是它不能变魔术一样,只要学会了就变出一个优美的设计出来,优秀的分析问题与解决问题的能力还是要靠不断地学习与借鉴他人成就才能得到提高。

  2. 测试驱动开发不能节省开发投入,也很少能够节省开发周期。测试开发所编写的大量测试代码都是要投入时间与精力的,我现在的代码统计显示,测试代码与实现代码的比例基本在3:2,即使因为测试驱动开发能得到一个简洁的设计,也不能弥补测试代码的工作量。当然,测试代码可以一定程度保证高质量的实现代码,从而减少后期软件测试与修正缺陷的工作周期,并进一步在软件发布后减少代码修正维护的工作量。但至少在开发阶段,两相抵消,开发周期并不能有明显改善,如果是第一次采纳测试驱动开发,甚至会延长开发周期。

  3. 测试驱动开发不能杜绝所有的软件缺陷。尽管测试驱动开发通过测试约束,减少了程序员犯错和遗忘的可能,但是这只是把问题从实现代码部分地转移到了测试代码。测试用例的完备与否,测试代码本身逻辑的正确与否都依赖于程序员,糟糕的测试用例设计和测试代码实现可能自顾不暇,也就失去了监督实现代码的能力。我就见过有程序员在测试代码中读取实现代码生成的数据,再直接拿之来验证实现代码生成的数据,x必然恒等于x,这样的测试逻辑必然成功,但是毫无意义。

  对测试驱动开发认识的深入,让我更能合理运用它,扬其长避其短,充分享受其带来的便利。

  测试驱动开发带给我前所未有的软件开发体验,人们都说TDD是传染病,一旦接触就无法自拔。我想说,是的,但我心甘情愿被传染,无怨无悔,无忧无虑。借用一句英文:There’s nothing to fear, TDD is with us, amen.

一些很好的评论:测试驱动开发常常提到The goal is clean code that works. 我非常赞同,其实不管使用什么选择的开发策略(或者是否认可使用TDD,我的很多朋友就不是很认可TDD),其实我们的目的都是写出“整洁”的代码得到使我们的软件在质量和产出方面,得到可观的改善。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值