推行TDD的思考

标签: tdd 思考 | 发表时间:2013-12-25 21:26 | 作者:
出处:http://agiledon.github.com/

目前来看,推行TDD的障碍大约有如下几点:

  1. 开发人员的质量意识;

2. 分析需求并进行任务分解的能力;

3. 将测试作为开发起点的开发习惯;

4. 开发人员的重构能力,包括如何识别坏味道和如何运用重构手法;

5. 单元测试的基础设施,尤其是测试数据准备;

开发人员的质量意识

开发人员对于软件质量,常常偏重于软件的外部质量,体现在他们的工作效益上,就是被测试人员发现的缺陷数。而惯常的软件开发思想,总是认为开发人员不适合做测试,因为他们总是站在自己的角度去看待问题,从而可能忽略真正需要测试的用例。这种思想给了开发人员一个错误信号,认为自己不应该写测试,即使写了测试,也写不好。殊不知,由开发人员编写测试带来的收益,最重要的一点不在于测试本身,而在于它能促进开发、测试以及需求分析人员的交流与沟通。而测试先行的方式也能让开发者跳出实现的窠臼,而从业务角度去看待问题,从消费者角度去思量接口。此外,由于开发者总是惫懒地将测试职责委派给了专门的测试人员,于是渐渐会产生一种依赖心理。测试人员的精确测试当然可以保障质量,但这种测试通常是黑盒测试,这里保障的质量主要还是外部质量。而且,这种测试带来的反馈总是慢于开发进度,一旦发现缺陷,修复缺陷的成本也会变得更高。

软件质量除了外部质量之外,内部质量同等重要。软件成本等于开发成本与维护成本之和,而维护成本的增加主要就归咎于内部质量的糟糕。这里讲的内部质量包括:代码的可读性、可重用性、可扩展性等。当我们让开发人员为原有代码编写单元测试时,总是觉得举步维艰。分析原因,主要问题在于代码的可测试性不好。要测试一个类,竟然连简单创建它的对象都变成了不可能完成的任务。我们为这样的代码编写单元测试,就好像在触及蜘蛛网,一旦被这些网丝给牵住,缠住,就可能无法摆脱。除非我们能够快刀斩乱麻,那对于这个系统而言,就不是维护,而是重写了。测试先行的开发至少在一定程度规避了这样的问题。即使代码的内部质量仍有所欠缺,但在足够覆盖率的保护下,我们要进行重构也变得更为简单。

然而,这些好处都不是短期能够见到成效的,且团队若不能达成共识,只靠一二人坚定地践行TDD,在测试覆盖率不够的情况下,改进仍然有限。多数开发者在维护别人的丑陋代码时,可能会骂声连连,殊不知同时作为骂者自身,其实也在重复被骂者的故事。

我不是说没有采用TDD,代码质量就一定不高;但我可以说采用了TDD,代码质量至少有了可以改进的基础。

分析需求并进行任务分解的能力

需求分析能力常常是开发人员的短板。开发人员养成了一个习惯,看什么事情都会从技术实现的角度去思考。要实现一个网页,就会想到如何编写JavaScript来响应用户的动作,如何编写CSS,却不会去思考用户体验和操作的流程。要完成一个数据分析,总会想到数据的属性,转换和提取数据的算法,却不会想到分析数据的价值以及合理的流程。

而且对于繁琐的需求描述,我们总没有耐心去深入研读,而是会在掌握了大体意思后,就开始匆匆进行开发与实现。TDD要求我们在编写测试之前要做好合理的任务分解。若没有很好地理解需求,任务分解就无法顺利的进行。

这就带来了团队协作的问题。若我们能从需求的源头上进行改进,或许TDD会变得更容易。例如,我们对故事的拆分更合理,较好地遵循了User Story的INVEST原则,那么我们所要实现的Story在测试性、独立性方面都会有很好的改观。如果BA能够非常明确地编写出验收条件(Acceptance Cretiria),进行任务分解就变得更加容易了。

更进一步,若BA能够参考甚至遵循Specification By Example,并采用Given-When-Then的模式来描绘各个用例场景,再要进行任务分解,不就变得轻而易举吗?因此,有时候推行TDD非常艰难,或许最大的原因是我们仅仅将目光放到了开发者身上,而忽略了BA扮演的关键角色。正所谓:问渠那得清如许,为有源头活水来。

我一直强调任务分解是有层次的。分析需求时,不能一个猛子就扎进繁琐的实现细节。要从用户价值出发,先梳理出最外层的需求任务,然后抽丝剥茧,条分缕析地层层递进,如此方能理清思路,掌控复杂逻辑。基本上,任务分解可以分为三个层次,即业务价值——>业务功能——>业务实现。并且这个层次是一种“递归”的状态,视需求的复杂度而定。

将测试作为开发起点的开发习惯

再说说开发习惯的问题。这种改变显然不是一朝一夕可以完成的。以我个人的经验以及我所观察到的情况来看,固然是习惯的力量在作祟,然而主因还是因为对TDD方法的掌握程度以及一些误解导致。

前面已经述及,任务分解应该是TDD的起点。多数开发者未能形成任务分解的习惯。因此在改变为测试先行的时候,错以为应该一上来就写测试。因为思路没有理清,脑子里是一片乱麻,再加上本身对TDD不够熟悉,于是编写测试就变得举步维艰,总觉得束手束脚,就好像被绑了一只手,又好像是在泥沼中挣扎。许多时候,甚至发挥不出自己的哪怕三分的功力。

一贯以来,我们都在强调测试先行,测试先行……容易产生一种错觉,就是认为TDD必须一开始就写测试,“简单设计”嘛,于是就没有了设计。这让那些习惯于事先设计的开发者更难以接受。

以下是我对于“TDD是否需要事先设计”的个人观点:

Martin Fowler的文章Is Design Dead?其实就是对此问题的正本清源。我个人认为,视场景而定,测试驱动开发仍可进行事先设计。设计并不仅包含技术层面的设计如对OO思想乃至设计模式的运用,它本身还包括对需求的分析与建模。若不分析需求就开始编写测试,就好像没有搞清楚要去的地方,就开始快步前行,最后发现南辕北辙。测试驱动开发提倡的任务分解,实际上就是一种需求的分析。而如何寻找职责,以及识别职责的承担者则可以视为建模设计。测试驱动更像是一种培养设计专注力的手段,就像冥想者通过盘腿静坐的手段来体悟天地一样,测试驱动可以强迫你站在测试的角度(就是使用者的角度)去思考接口,如此才能设计出表现意图的接口。但编写测试自身并不能取代设计,正如盘腿静坐并不等于就是冥想。

在开始测试驱动开发之前,做适度的事先设计,还有利于我们仔细思考技术实现的解决方案。它与测试驱动接口的设计并不相悖。解决方案或许属于实现层面,若过早思考实现,会干扰我们对接口的判断;但完全不理会实现,又可能导致设计方向的走偏。举例来说,如果我们要实现XML消息到Java对象的转换。一种解决方案是通过jaxb将消息转换为Java对象,然后再定义转换映射的Transformer,通过硬编码或者反射的方式将其转换为相关的领域对象。然后在执行了业务操作后,再将返回的结果转换为另一个Jaxb对象。而另一种解决方案则是通过引入模板,例如StringTemplate或者Velocity,定义转换的模板,然后进行替换实现。这两种解决方案的区别,直接影响了我们划分任务的方式。所以在运用TDD时,先不要一巴掌拍死,可以先抱着开放的态度尝试尝试。何况,TDD并非一招鲜,吃遍天,总要有适合它的场景。例如UI的开发,交互协作的控制逻辑,数据库开发,并发处理,都不是运用TDD的太好场景。

开发人员的重构能力

TDD的核心是红——绿——重构。这意味着重构是TDD非常重要的一环,它直接关系到TDD开发出来的代码质量。没有好的重构能力,TDD就会有缺失。若说代码的内部质量是生命的话,重构就是灵魂,缺少了它,代码就没有灵性了。多数时候实施TDD,都会因为重构能力的缺乏而陷入困境。

重构的关键首先在于如何识别代码的坏味道。这需要代码阅读的千锤百炼,而非死记硬背老马总结的坏味道。当这些坏味道变成你的一种直觉,甚至就像与生俱来的一种能力时,你就会降低对糟糕代码的容忍度。在你眼中,这些烂代码就是垃圾,必须清扫,否则无法“安居”。

重构手法与代码坏味道一一对应。若有测试保障,重构就变得安全。但尽可能地,我们还是希望运用工具提供的自动重构功能,这既提高了重构效率,也在一定程度下确保了重构的安全。

当然,重要的是要找到重构的节奏感,即小步前行,每次重构必运行测试的良好习惯。若能结合分布式版本管理系统,做到原子提交,就会更加方便。即使重构出现问题,我也可以快速地回到前面的版本快照。

在TDD过程中,若能结对自然是上佳选择。当一个人在掌控键盘时,另一个人就可以重点关注代码的可读性,看看代码是否散发出臭味。两个人的眼睛终归要更锐利一些,至少视野的范围更广泛。

及时重构是重构诸多实践中最重要的一点。不要让重构成为你在未来偿还债务的杀手锏。越拖到后面,偿还债务的成本就越高。以重构而论,可能需要的重构能力就更强,因为重构变得复杂了。当然,只要你的代码能够保证足够的覆盖率,以及较好的松散耦合,重构依旧可行。采用TDD,基本能满足这两条要求。但以成本而论,小步前行才是重构之道。

单元测试的基础设施

最后说说单元测试的基本设施。很多时候,这可能不是问题;但很多时候,这可能会成为大问题。面对诸如测试数据准备等问题,需要认真分析,找到应对方案。原则上最好能找到一些开源的测试框架,包括生成测试数据,模拟测试行为等……多数情况下,这些开源框架都已经提供了。因为你遇到的问题,别人可能早已遇见过。这个世界上有很多聪明而又乐于分享的程序员,不要局限在自己公司一隅。睁大眼睛看看满世界吧。所谓“君子生非异也,善假于物也”。好程序员,也要这样。

说不定,你会抛弃TDD,因为你找到了更好的适合你的做法。

相关 [tdd 思考] 推荐:

推行TDD的思考

- - 简单文本
目前来看,推行TDD的障碍大约有如下几点:. 分析需求并进行任务分解的能力; 3. 将测试作为开发起点的开发习惯; 4. 开发人员的重构能力,包括如何识别坏味道和如何运用重构手法; 5. 单元测试的基础设施,尤其是测试数据准备;. 开发人员对于软件质量,常常偏重于软件的外部质量,体现在他们的工作效益上,就是被测试人员发现的缺陷数.

JavaScript TDD 神器jasmine

- - JavaScript - Web前端 - ITeye博客
今天参加了圣路易斯本地的一个meet up group. 演讲主题是javascript 的tdd. 演讲者展示了jasmine的功能,真的是神器啊. 以下是jasmine的网址:. jasmine的syntax 极其简单:. 还有很多功能还在探索中,在写2500行的js之前知道这个就好了. 已有 0 人发表留言,猛击->> 这里<<-参与讨论.

Bob大叔和Jim Coplien对TDD的论战

- Duo - 酷壳 - CoolShell.cn
今年春节时,我写了一篇《TDD并不是看上去的那么美》,在这篇文章中我列举了一些关于使用TDD的一些难点和对TDD的质疑,后来出现了一些争论(可参见那篇文章的评论),以及Todd同学的《TDD到底美不美》,还有infoQ中文上的那个几乎没有营养离线讨论. Coplien的很多观点和我之前的不谋而合,而他自己称他是坚决强烈地站在TDD的对立面上.

在团队中进行单元测试/TDD的12条经验

- - ITeye资讯频道
两年前,我在一个Web项目开发组中,项目的目标是编写一个类似Excel的、用来计算产品/服务价格的Web应用程序. 项目团队被分成3部分——开发团队、需求团队和QA团队. 随着项目越做越大,而我们没有使用任何形式的自动化测试(QA团队使用手工测试),结果导致项目的测试时间比开发时间还要多. 每进行一次小的改动,QA团队都要花费几个小时来做测试.

是否使用TDD(测试驱动开发)进行UI开发

- - SegmentFault 最新的文章
StackOverflow上有一则 是否使用TDD(测试驱动开发)进行UI开发 的提问. 对于是否使用TDD进行开发UI这件事,我想了很久,但难以决定. kdgregory的回答(23票赞同). 试图测试UI组件的放置是没有意义的,首先因为UI布局是主观的,所以应该由人来测试. 其次,随着UI改动,你要不断地重写测试.

为什么 WCDMA 可以边通话边上网,而 TDD LTE 不能?

- - 知乎每日精选
这个问题给牛小哥点了赞同,Duncan Zhang的答案大体上是对的,但是不得不说,LTE不论时分还是频分双功是一定而且必须可以同时进行数据和话音业务的. WCDMA最开始的时候从GSM发展而来,因而保留了GSM的核心网部分,所谓核心网,简单来说就是一个交换网络,负责把数据请求和呼叫请求接到不同的网络上,在GSM还没有GPRS那会,基本上只有呼叫请求,而当时的通信是建立在电路交换的基础上,电路交换简单说就是有信道建立和信道拆除,通信中独占信道方式,通常的PSTN(即电话网)就是电路交换,强调可靠;而互联网是IP网络,基于分组交换,强调有效.

终极思考

- wei - 牛博国际
我的海淀剧院演讲门票放出后,八小时卖了四百多张,同事们说,日. 我淡淡地说,别这样,也许正是因为便宜才这么好卖嘛. 一转身我马上就打电话给老婆,操. 早知道就他妈把票价定高一点啦,真倒霉......干. 很大程度上,这可以解释两件事:1.为什么已婚事业男性的健康状况会相对好一些. 2.为什么在社会上受到尊重和认可的事业男性在老婆的眼里都是傻逼.

动车追尾的思考

- David Ruan - 扬韬
1、两列运行的动车追尾,绝对属于重特大责任事故. 雷电导致前车失灵,已经是责任事故了. 前车失灵,信号没有外发,又是责任事故. 调度体系没有发觉列车失灵,也是责任事故. 后车没有察知前车失灵,还是责任事故. 最后,后车发现问题,紧急制动系统有没有用也值得怀疑,因为后车司机据说是人工制动并殉职于岗位的.

重新思考电子书

- Alex - 爱范儿 · Beats of Bits
Hart,“古登堡计划”发起人,2011 年 9 月 6 日去世,享年 64 岁. 从 1971 年 Hart 制作第一本电子书,启动“古登堡计划”开始到 2011 年,Kindle、Nook 流行,正好经过 40 年. 如今电子书阅读器、电子书变得越来越流行,在北京的地铁上,你会经常看见低头拿着 Kindle、Nook、iPad、汉王的人们.