测试驱动开发
一:测试驱动开发所追求的目标:
代码整洁可用。
二:测试驱动开发的基本过程如下:
1) 明确当前要完成的功能。可以记录成一个 TODO 列表。
2) 快速完成针对此功能的测试用例编写。
3) 测试代码编译不通过。
4) 编写对应的功能代码。
5) 测试通过。
6) 对代码进行重构,并保证测试通过。
7) 循环完成所有功能的开发。
如图:
三:使用驱动开发可以让你:
知道什么时候完工,而不用担心是否会长期被bug无休止的困扰。
全面正确的认识和利用代码的机会,如果你总是草率地利用你最先想到的方法,那么你可能再也没有时间去思考另一种更好的方法。
让软件开发小组成员之间变得相互信赖。
…
四:以下是摘录自网上某开发者使用TDD的心得:
测试驱动开发意味着不再是从需求分析与概要设计/详细设计后直接进入到实现代码的编写,而是转而根据需求分析和概要设计进行测试用例的设计与测试代码的编写,通过测试代码硬性规定了实现代码所必须满足的功能需求、容错能力。编写实现代码的唯一目的就是使所有测试用例成功运行,任何测试用例的失败都意味着实现代码存在功能缺陷或者逻辑错误。之后就是不断重复--修改或增加代码再运行所有测试用例检查结果--的迭代过程。
测试驱动开发的优势:
1. 有助于设计简单清晰而易用的接口。
因为总是先有测试代码,才编写实现代码,意味着总是从使用者的角度设计接口,只有简单易用的接口才方便测试时调用,所以我几乎是“被迫”去努力设计简单易用的接口,因为我就是第一个使用者。
2. 模块切分的足够小但是模块间保持极低的耦合度。
为了方便测试,我总是尽力把重复的逻辑剥离出来,单独构建模块进行测试;并且尽量减少模块间的耦合,保持模块相对独立和功能完备。如果模块过大,或者模块间强耦合,写测试用例与代码时就会困难重重,笨重复杂。因为总是先写测试代码,眼前的利益高于一切,将来的实现代码必然要迁就目前测试的需要。于是,我又“不知不觉”设计出小粒度模块,且模块间耦合度低的实现。
3. 肆无忌惮的重构。
因为有测试用例和测试代码作担保,我终于能够从小心翼翼心惊胆战的重构中解脱出来,只要是更好的实现和设计,我都愿意尝试,管它呢,反正多跑几遍测试就知道重构的结果如何了。可以说,测试驱动开发鼓励代码的不断进化,即使测试已经全部通过,也可以通过大胆重构来改进设计与实现。尽管此时是否还需要再重构见仁见智,至少提供了一个可能,我是很喜欢这一点。
4. 测试代码是“活”的软件文档,它硬性规定了实现代码必须满足的需求,达不到就报错。
传统的文本文档比之就苍白无力多了,“应该”,“必须”,这些字眼对程序员有多少约束力?而且测试代码总是能与实现代码保持新鲜同步,传统文档写完后经常被上传服务器束之高阁,很少人问津,随着开发组内人员的变动,往往最后就湮没在服务器的故纸堆之中了。
测试驱动开发竟不是软件银弹,也不存在这样的银弹,它也有力不能及的地方:
1. 测试驱动开发不可能让人立即具有设计出优美解决方案的能力,或者说是优秀的分析与解决问题的能力。TDD不是Test Driven Design。它只是一个过程,也许可以帮助你发现并帮助你实现优美的解决方案,但是它不能变魔术一样,只要学会了就变出一个优美的设计出来,优秀的分析问题与解决问题的能力还是要靠不断地学习与借鉴他人成就才能得到提高。
2. 测试驱动开发不能节省开发投入,也很少能够节省开发周期。测试开发所编写的大量测试代码都是要投入时间与精力的,我现在的代码统计显示,测试代码与实现代码的比例基本在3:2,即使因为测试驱动开发能得到一个简洁的设计,也不能弥补测试代码的工作量。当然,测试代码可以一定程度保证高质量的实现代码,从而减少后期软件测试与修正缺陷的工作周期,并进一步在软件发布后减少代码修正维护的工作量。但至少在开发阶段,两相抵消,开发周期并不能有明显改善,如果是第一次采纳测试驱动开发,甚至会延长开发周期。
3. 测试驱动开发不能杜绝所有的软件缺陷。尽管测试驱动开发通过测试约束,减少了程序员犯错和遗忘的可能,但是这只是把问题从实现代码部分地转移到了测试代码。测试用例的完备与否,测试代码本身逻辑的正确与否都依赖于程序员,糟糕的测试用例设计和测试代码实现可能自顾不暇,也就失去了监督实现代码的能力。我就见过有程序员在测试代码中读取实现代码生成的数据,再直接拿之来验证实现代码生成的数据,x必然恒等于x,这样的测试逻辑必然成功,但是毫无意义。
五:测试驱动开发的原则:
51 测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。
5.2 一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。
5.3 测试列表。需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。
测试驱动。这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。
5.4 先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。
5.5 可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。
5.6 及时重构。无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。关于重构,我会另撰文详细分析。
5.7 小步前进。软件开发是个复杂性非常高的工作,开发过程中要考虑很多东西,包括代码的正确性、可扩展性、性能等等,很多问题都是因为复杂性太大导致的。极限编程提出了一个非常好的思路就是小步前进。把所有的规模大、复杂性高的工作,分解成小的任务来完成。对于一个类来说,一个功能一个功能的完成,如果太困难就再分解。每个功能的完成就走测试代码-功能代码-测试-重构的循环。通过分解降低整个系统开发的复杂性。这样的效果非常明显。几个小的功能代码完成后,大的功能代码几乎是不用调试就可以通过。一个个类方法的实现,很快就看到整个类很快就完成啦。本来感觉很多特性需要增加,很快就会看到没有几个啦。你甚至会为这个速度感到震惊。(我理解,是大幅度减少调试、出错的时间产生的这种速度感)
6.1. 测试范围、粒度
对哪些功能进行测试?会不会太繁琐?什么时候可以停止测试?这些问题比较常见。按大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。感到疲劳就应该停下来休息一下。感觉没有必要更详细的测试,就停止本轮测试。
测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据你的经验,功能复杂、核心功能的代码就应该编写更全面、细致的测试用例,否则测试流程即可。
测试范围没有静态的标准,同时也应该可以随着时间改变。对于开始没有编写足够的测试的功能代码,随着bug的出现,根据bug补齐相关的测试用例即可。
小步前进的原则,要求我们对大的功能块测试时,应该先分拆成更小的功能块进行测试,比如一个类A使用了类B、C,就应该编写到A使用B、C功能的测试代码前,完成对B、C的测试和开发。那么是不是每个小类或者小函数都应该测试哪?我认为没有必要。你应该运用你的经验,对那些可能出问题的地方重点测试,感觉不可能出问题的地方就等它真正出问题的时候再补测试吧。
6.2. 怎么编写测试用例
测试用例的编写就用上了传统的测试技术。
- 操作过程尽量模拟正常使用的过程。
- 全面的测试用例应该尽量做到分支覆盖,核心代码尽量做到路径覆盖。
- 测试数据尽量包括:真实数据、边界数据。
- 测试语句和测试数据应该尽量简单,容易理解。
- 为了避免对其他代码过多的依赖,可以实现简单的桩函数或桩类(Mock Object)。
- 如果内部状态非常复杂或者应该判断流程而不是状态,可以通过记录日志字符串的方式进行验证。
重构
------改善既有代码的设计
重构:在不改变可见部分的行为的前提下,改变软件的内部结构,使之更加易于理解和修改。
重构:通过使用一系列refactoring,改变软件的结构,而不改变它的外部表现
重构的目的:
1.改善软件的设计;
2.使软件更加容易被理解;
3.帮助我们发现缺陷;
4.加快编码的速度;
5.使软件容易维护。
什么时候重构:
1.在增加功能时;
2.在修复缺陷时;
3.在回顾代码时
为什么要重构
因为我们编写的代码会有一些很不好的设计,当我们遇到下面一些不好的设计时我们就要考虑重构了:
1.重复的代码.
如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将他们合二为一.
2.过长的函数.
越短的函数会存活的时间更长,存活的更好.
3.过长的类.
如果想利用单一的类做很多的事情,那么该类的内部会出现很多的instance变量,重复代码就要接踵而至了.
4.过长的参数列.
太长的参数列难以理解,太多的参数会造成前后不一致,不易使用,一旦你需要更多的数据,就不得不修改它.
5.发散式变化.
一旦我修改软件,我希望只在一处修改就好,如果不能做到这点,就应该考虑到重构了./
6.烟雾弹式修改.
一旦软件进行修改,你必须去对多个类的内部做小修改, 这时就应该考虑到重构了.
7.依恋情结.
函数对某个类的兴趣高过对自己所处之host类的兴趣, 这时就应该考虑到重构.`
8.数据泥团.
两个类中的相同值域,多个函数中的相同参数, 这时也就应该考虑到重构了.
9.基本类别偏执.
如果一个类只为了做一两件事而创建,却付出了太大的额外开销的话,就应该考虑到重构了.
10.switch惊悚现身.
尽量少用switch语句,因为switch语句的问题在于重复.
11.平行继承体系.
如果你发现某个继承体系的类名称前缀和另一个继承的类名称前缀完全相同,这时也应该考虑重构
12.冗赘类.
如果一个类的所得不值其身价,消失吧.
13.夸夸其谈未来性.
14.令人迷糊的暂时值域.
某个instance变量仅为某种特定情况而设置.
15.过度耦合的消息链.
16.中间转手人.
讨厌的封装,对外部世界隐藏其内容.
17.狎昵关系.
两个类过于亲密,花费太多的时间去探究彼此的似有成分.
18.异曲同工的类.
如果两个方法做同一件事,却有不同的名字.
19.不完美的程序类库.
20.纯稚的数据类.
该类的特性是,拥有一些值域,一级用于访问这些值域的函数,其他的一无所有.
21.被拒绝的遗赠.
子类应该继继承父类的方法和数据,但是父类都写成似有的,不希望子类继承,这时应该考虑重构
22.过多的注释.
你发现一个类有很多的注释,是因为这个类很烂,那么这里也应该考虑重构.
面对上面需要重构的地方,我们采用的重构的方法我们通常有以下一些