TDD到底美不美?

最近CoolShell上的一篇 《TDD并不是看上去的那么美》 引起了敏捷社区的高度关注和激励辩论。今天,InfoQ甚至专门举行了一个“虚拟座谈会” 《TDD有多美?》 ,几位国内敏捷社区的名人专门就此问题展开了深入地讨论。不论结果如何,这种探讨和反思的精神还是非常值得赞赏的。事件实际上可以简单地归纳为“一个有一定影响力的开发人员质疑TDD,一群敏捷社区名人对TDD进行解释和辩护”。现在,就让我坚定地站在CoolShell一边,为对TDD的质疑和批判添砖加瓦吧!

我们首先来看看TDD的核心理念是什么。第一是“用例即规范”(Specification by Example),即把测试用例作为需求规范的一种形式。传统的需求表达方式包括文档,Use Case等,而TDD强调通过测试用例来表达需求。另外,TDD的测试用例是黑盒的基于外部接口的,所以,它实际上又是对外部接口的设计。“不把测试用例单纯地视为测试,而从需求和设计的角度来看测试用例”是TDD与传统测试的一个重要区别。TDD的第二个重要理念是Test First,强调测试对于实现的驱动作用,先写测试用例,再实现和重构。Test First的实质是“先理解清楚需求,并做好外部接口设计,把它转化为测试用例,然后再来实现和重构”。

如果说“用例即规范”还弥补了文档和Use Case在表达需求时的某些不足,具有一定的好处,那么Test First则有很大的问题,尤其“在没有测试用例失败之前,不要写任何一行代码”的极端方式则更是极端的错误。

如果测试用例就是需求和设计,那么为什么不能先写出测试用例再来实现呢?这不是我们最熟悉的先需求再设计再编码吗?答案是:不能执行的测试用例(Test First)和能执行的测试用例有着天壤之别,你写出了测试用例不代表你就看到了运行的实际效果。不能执行的测试用例和写在纸上的文档相比对实现的指导意义不见得能好到哪里去!除非是一些很简单的情况下,在实际的软件开发中,你很难在没有执行测试用例的情况下写出真正符合最终需求的测试用例来。比如:你做一个页面,页面的效果需求和设计通常会在真正可以运行之后不断调整,在实现之前只能有一个大致的轮廓和方向,许多方面的细节要么是没想清楚,要么是完全没想到,不可能一蹴而就。如果片面强调测试对实现的驱动作用,那么实际上隐含了“需求和设计的细节可以在实现之前明确下来”的假设,这是非常不敏捷的和不现实的!

Test First要求写测试用例时对软件需求有精确的了解,但实际软件开发过程中用户需求和外部环境的不确定性会导致软件需求难以把握和频繁变动。

用户需求的不确定性是指“需求无法在用户真正能运行看到效果之前明确下来”。比如:让你开发一套Wow这样大型的游戏,你能想象游戏的效果是设计者一开始就想好了精确到每一个细节吗?对于游戏这样的软件,需求和设计不可能脱离实际运行纸上谈兵地产生。游戏的设计者通常只能借助文档、草图、Use Case等非精确的方式大致提出需求,先做出原型,在看到效果之后才能逐步地细化和明确,需求设计的增加和改变会伴随整个软件开发过程。另外,还有一种极端的情况是根本不存在精确的用户需求,比如:自动化翻译软件,你能在实现之前就把翻译效果用测试用例固定下来吗?存在绝对正确的翻译方法吗?最近,我们和国外一家大公司客户谈一个项目需求的时候,客户讲了这样一句话“我们现阶段还无法提出很细致的需求,只有等你们拿出第一个版本,然后我们再逐步地调整细化”。我们的客户没有宣称自己在做敏捷,但人家的思维方式多敏捷啊,不是什么一上来就明确需求,而且还要精确写出自动化测试用例。有人说这种情况我们仍然可以先根据自己的理解进行TDD,这样做可以:1.基于测试用例和客户沟通明确需求;2.驱动实现。我对此持不同看法,能执行的测试用例和不能执行的测试用例有着天壤之别,客户从测试用例根本无法获得真实运行的体验,你能想象苹果把iPhone的测试用例写在PPT上,给用户做一个演讲,用户就能给出关于iPhone设计的反馈了吗?要真正的用户反馈,就需要实打实的软件,这不正是敏捷的“Working software over document”的思想吗?另外,既然用户无法在实际体验之前提出反馈,那么开发人员在开发初期做的需求分析和设计都只是一个探索,随时可能调整甚至被推翻,不值得在实现之前进行自动化测试设计的投资。

外部环境的不确定性是指"当我们的系统需要和外部系统集成时,关于外部系统行为的假设也无法在实际集成运行前完全确定"。例如,要做一套股票客户端连上交易所系统,因为交易所的行为会直接影响到客户端的开发,所以只有在弄清交易所行为的情况下才谈得上开发出高质量的客户端。如果采用测试驱动,编写了各种涉及交易所行为的测试用例,比如什么情况下发什么类型的消息,消息格式如何,如何交互等等,但是这些测试用例本身是否正确却需要打一个大大的问号!这一方面是由于很多交易所提供的协议都不够清晰或者有许多未明确定义的地方;另一方面即使协议没有问题,开发人员也可能由于单纯的失误或者缺乏相应领域的基本知识而把协议理解错。实际上,要真正弄清交易所的行为明确客户端的需求,最重要的手段还是在交易所提供的测试环境中跑集成测试。对于Test First来讲,测试用例本身的错误可以说是代价最大的,不仅浪费时间和精力,更重要的是还打击开发人员的士气,谁愿意来回折腾呢?但很不幸,实际情况是在最初没有明确交易所行为的时候Test First出来的测试用例随时可能在真实集成后被推翻,并且如果是比较高层的需求分析失误,那对整个架构设计来讲会是灾难性的后果。在实际开发中,我们的软件需要和其他系统集成的情况是非常普遍的,而期望在没有进行实际集成的情况下弄清外部系统的行为都是不现实和不敏捷的。

所以,Test First需要对于被测系统的需求和环境有精确的了解,但由于需求不确定性和外部环境不确定性两大问题,Test First在很多时候都是不现实的。其实,Test First和瀑布式思想一脉相承,都强调需求先于实现,而忽略了软件需求的产生会受到实现的反馈,会在实际运行中不断调整探索完善。TDD无非是把需求分析的结果用测试用例表达,替代传统用文档表达需求,但从宏观上看,TDD和瀑布比是换汤不换药,这都不是真正的敏捷。除了简单情况,不存在脱离实现的需求,你能够在明确了需求之后就实现出一套Linux系统吗?既然你根本无法实现一套Linux系统,那么这样所谓的需求又有多大的意义呢?所以,能提出什么样的需求不能脱离你的实现能力。需求和实现之间不是简单的谁驱动谁,而是一种相互反馈的关系,这与需求用什么方式表达没有关系。正如瀑布模型无法在初始阶段做出完美的需求分析,TDD也无法在初始阶段做出完美的测试用例;不仅如此,自动化测试用例的开发维护成本还远高于文档。所以,在敏捷环境中,软件开发初期应该通过文档和用例等手段大致表达需求,实现之后在实际运行中体验效果,不断优化探索和明确需求和外部环境,当需求和对外部环境的认识达到一个比较稳定的程度才编写测试用例将需求固化下来。

上面的论述主要针对贴近最终用户的外部需求(如ATDD),下面我会进一步解释即使是在内部的单元测试级别TDD仍然有问题。我们还是首先从需求入手,思考一下单元的需求是哪里来的呢?答案是:需求来自于设计!比如,对轮胎的需求来源于汽车的设计,低层模块的需求来源于高层模块的设计。而在开发初期,这种内部设计具有很大的不稳定性,带有很多假设的成分,在没有进行集成测试的情况下,很难讲这种内部设计是否合理。实际项目开发通常会在集成运行之后不断调整内部的设计,即影响单元的需求。那么,如果是测试驱动,首先按不成熟的内部设计把一个个单元需求编写成单元测试再来实现,实际上大大推迟了能进行集成测试的时间,对于真正快速弄清高层需求稳定设计反而是不利的。假设最终还是所有单元都完成,然后开始运行集成或验收测试,这时候有两种可能:1.用户看到实际效果,决定调整需求;2.发现集成前在单元层面的假设不成立或者是有没有考虑到的情况。不论是哪一种情况发生,以前所写的单元测试都面临着被废弃或必须修改的命运。实际上,多数与业务相关的单元测试用例比起集成或验收测试用例更加不稳定,因为它会受到所有其上层模块的需求和设计变动的影响。由于我们在不稳定的单元测试上浪费了大量的时间(按我的经验编写单元测试比编写实现更耗时),这就导致了迟迟无法进行集成看到实际效果,也没有办法敏捷地应对需求的调整。也就是说具有讽刺意味的, Test First理念居然是和敏捷理念矛盾的!

所以,我认为Test First不符合敏捷开发的基本假设,而真正符合敏捷的理念是“需求和设计依赖于实现的反馈,需要在实际运行过程中根据效果不断探索调整得来的,不可能脱离实际运行写出真正符合最终需求的测试用例来”。所以,我们真正应该做的是尽快看到实际运行的效果,而自动化测试作为固化的需求和设计是在看到效果之后。在集成之前花太多精力进行测试驱动只会导致迟迟看不到实际运行效果(尤其是基于开发人员自己的假设编写大量单元测试用例),看到效果需要调整需求又会废掉或改掉一大堆的测试用例。实际上,越是外部的需求其变更带来的影响和代价越大,越是需要尽早明确。从宏观上看,TDD所谓的快速反馈实际上是加快内部反馈,延迟了外部反馈,这无异于本末倒置。而大量需要修改或作废的测试用例其实是一种很大的浪费,这和消除浪费的精益思想也是矛盾的!


上面这幅cost/length_of_feedback_cycle图是我们常见的用于说明敏捷方法比传统方法具有更短的反馈周期,更小代价的应对变化。从图中我们可以清晰的看到在验收测试中发现的需求错误导致的代价是最高的。如果验收测试往后推迟一点,发现错误的代价将按非线性地增长。上面我们已经论述了,任何方法都不可能消除验收测试后对需求的调整,因为这是需求产生的正常过程。我们唯一可以做的是尽可能地缩短验收测试的反馈周期,但是很不幸TDD大量的内部测试只会导致推迟验收测试的时间,从而大大增加代价。在实际开发中,我提倡在第一次集成运行测试之前不要写单元测试用例;自动化的验收测试用例则视编写和维护的代价而定,如果代价比较高,则应该采用文档和Use Case来描述需求,因为这两种方式比自动化的验收测试更容易维护。编写单元测试一定是在集成以后,这样才能首先得到外部反馈,尽量先保证做正确的事情,再正确地做事

下面这段话来自于InfoQ文章 《Mock不是测试的银弹》 :“在使用JMock框架后测试编写起来更容易,运行速度更快,也更稳定,然而出乎意料的是产品质量并没有如我们所预期的随着不断添加 的测试而变得愈加健壮,虽然产品代码的单元测试覆盖率超过了80%,然而在发布前进行全面测试时,常常发现严重的功能缺陷而不得不一轮轮的修复缺陷、回归 测试。为什么编写了大量的测试还会频繁出现这些问题呢? ”这描述的情况和我在实践中遇到的情况类似,不过很可惜文章并没有找到问题真正的原因。真正的原因不是什么Mock不Mock,而是TDD的单元测试是基于开发人员的假设,这些假设的测试即使全部通过代码覆盖率100%,到了集成测试发现假设根本不成立或者原先在单元层面很多情况没有考虑到,这又怎能保证高质量?在TDD的实践者中我见到过不少类似这样的,他们很认真,编写了很多单元测试用例,代码覆盖率也很高,但他们其实是有意无意在先正确地做事(单元测试),再做正确的事(集成测试),这就是本末倒置。

当然,我不是全盘否定TDD。TDD在某些需求比较固定的场合是适用的,尤其是与具体业务关系不大的需求,比如:写一个通用的数据结构,实现一个通用算法。TDD的先关注需求和思考外部接口设计的理念也对促进开发人员的抽象思维有很大益处。另外,TDD通常也具有较高的代码覆盖率。本文的主要观点在于:实际项目中,由于用户需求不确定性和外部环境不确定性,不要期望可以在实现之前完全明确需求,需求是在实际运行看到效果之后才逐步明确的;我们的开发过程必须能够敏捷地适应需求的变化,而TDD的Test First理念恰好与之矛盾。所以,对于TDD不了解的朋友,我建议应该学习和实践TDD,从而获得其益处;同时我也提醒TDD存在理论上的缺陷,这是在实践中需要特别留意的。

4
2
(请您对文章做出评价)
« 博主前一篇: 聊聊结对编程
» 博主后一篇: 软件需求的薛定谔之猫

posted on 2011-02-23 22:42 Todd Wei 阅读(5638) 评论(57) 编辑 收藏

1 2

评论

#8楼 2011-02-24 00:00 Nick Wang (懒人王)

@ Todd Wei
不管你是先写测试还是后写测试,测试都是用来验证需求是否实现的,当需求改变时,测试用例自然会随之变化,跟test first还是test after没有关系。

#9楼 2011-02-24 00:03 Nick Wang (懒人王)

@ Ivony...
TDD不是没有设计,而是你用TDD的时候没有做设计。拿着一把锤子钉钉子,结果定歪了,你能说是锤子的问题么?

#10楼 2011-02-24 00:07 Nick Wang (懒人王)

现实一点看,你要是觉得TDD好,不管是很好还是有一点好,只要你觉得好,那你就用。你觉得不好,那就不用。

你觉得Test First接受不了那就Test After。没必要非争来争去,一定要下个结论说TDD不是那么的好。

给你一辆F1,还真不一定能比QQ开着舒服。

#11楼 2011-02-24 06:58 金色海洋(jyk)

把潜水的都给砸出来了呀。

能挣到钱就好,管他美不美呢?

#12楼 2011-02-24 08:53

@ Nick Wang (懒人王)
哥们 又是你。哈哈哈

咱们之前也血拼过了。

#13楼 2011-02-24 08:58 szwe

如果需求允许的话,没有人不希望是瀑布开发吧,不单单是维护文档的问题,还有责任和风险的问题。TDD虽然维护的强度增加了,但是责任和风险也减小了,最起码在开发之前你对很多方面(比如性能,易用性和扩展性)心里有个数了,不会陷入客户无休止的改进要求里面。

#14楼 2011-02-24 09:27 Ivony...

TDD的Test first可以理解为Build test before coding,but after design。

在一个粗略的设计之前是不可能将需求转化为计算机所能识别的形式的,不论是测试用例还是功能代码。

敏捷是一种开发思想,其核心价值观在于做最有价值的事情,是不是测试先行很大程度上取决于测试是否有价值。正如我上面所说,除了测试用例,优秀和具备良好习惯的程序员也可以确保代码在经过有限的自动化测试后的可靠性。但是依赖于优秀和具备良好习惯的程序员是不靠谱的事情,我们说一个模式就是确保在任何情况下问题都不会变得太糟糕,相信每个人都有过维护一坨糟糕的代码的经验。

OK,还是那句话,优秀和具备良好习惯的程序员写出一坨糟糕的代码的概率很低。但是当你不确定你的团队是怎样的情况的时候,测试先行至少能确保那一坨可能出现的糟糕的代码在按照预期工作,也能避免程序员不停的打断你的思维探讨实现细节和需求理解。事实上说白了这些事情是毫无价值的,当你的团队被这些无谓的沟通耗尽所有的时间的时候,你会发现其实简单粗暴的“构建测试—使其通过”模式会是一件高效的事情。

#15楼 2011-02-24 09:28 chenping2008

所以人是一个主要的因素

#16楼 2011-02-24 09:40 Ivony...

至于ThoughtWorks和刀刀,我想说的是ThoughtWorks是一家伟大的公司,从刀刀的描述中我们也能体会到ThoughtWorks的文化和执行力。很多时候我们说一个模式为什么在TW能够成功而在我的团队举步维艰,这并不是模式的问题,而是你是否具备TW那样的执行力?是否深刻的调查和理解你团队当前的问题?是否了解模式会解决和带来的问题以及一套行之有效的执行方案。否则总会遇到水土不服的问题。

#17楼 2011-02-24 10:44 横刀天笑

@ Ivony...
你是在说我么。。。

#18楼 2011-02-24 11:47 Ivony...

引用 横刀天笑:
@Ivony...
你是在说我么。。。


嗯,,,

#19楼 2011-02-24 11:51 横刀天笑

@ Ivony...
额。。。我还是去写技术文章得了,不趟方法论这个浑水,呵呵

#20楼 2011-02-24 11:54 徐少侠

Test first是原则
可以被简化,但不能被推翻
编写产品代码前必须有测试意识

#21楼 2011-02-24 11:55 Ivony...

引用 横刀天笑:
@Ivony...
额。。。我还是去写技术文章得了,不趟方法论这个浑水,呵呵


技术不是值钱的东西,或者说值钱的技术付出的成本太高了。方法论反而在更多情况下更值钱,最后,大家也希望多了解一些TW的方法论哈。所以还是趟趟这浑水吧,尽管这些争论最终是不会有一个结果也不存在正确答案的。

#22楼 2011-02-24 12:21 卡通一下

引用 Ivony...:
引用横刀天笑:
@Ivony...
额。。。我还是去写技术文章得了,不趟方法论这个浑水,呵呵

技术不是值钱的东西,或者说值钱的技术付出的成本太高了。方法论反而在更多情况下更值钱,最后,大家也希望多了解一些TW的方法论哈。所以还是趟趟这浑水吧,尽管这些争论最终是不会有一个结果也不存在正确答案的。

简单问题复杂化;一般问题“宝马”化,空谈者的特征,哈哈...

#23楼[楼主] 2011-02-24 12:52 Todd Wei

@ EricZhang(T2噬菌体)
>>但是我知道TDD在ThoughtWorks公司内部应用非常广泛并取得了很大成功。

下面这段话来自TW公司的文章
Mock不是测试的银弹
“的确确在使用JMock框架后测试编写起来更容易,运行速度更快,也更稳定,然而出乎意料的是产品质量并没有如我们所预期的随着不断添加 的测试而变得愈加健壮,虽然产品代码的单元测试覆盖率超过了80%,然而在发布前进行全面测试时,常常发现严重的功能缺陷而不得不一轮轮的修复缺陷、回归 测试。为什么编写了大量的测试还会频繁出现这些问题呢?”

这段话客观地描述了TW使用TDD遇到的问题,和我实践中遇到的情况相似,可惜文章并没有真正找到TDD的问题。真正的原因不是什么Mock不Mock,而是TDD的单元测试是基于开发人员的假设,到了集成测试才发现假设根本不成立。

#24楼[楼主] 2011-02-24 13:18 Todd Wei

@ 横刀天笑:
>>外部需求可以不稳定,但还是不妨碍我们使用TDD来提高内部质量。这是其一。 ... 但是反馈的环是一环套一环的,越往外的环代价会越大。我们为什么不构建一些内环呢?
正因为越往外代价越大,我们才需要尽快进行集成,写内环的单元测试会大大拖慢集成的时间,而一旦集成又会废掉或改掉一大堆单元测试。

>>需求变了为啥不可以改测试。那么如果采用Test after的方式,需求变了我们就可以不改测试了么
TDD才有改不改的问题,如果我快速集成让需求稳定,然后test after,这就不是修改了。可见,test after比test first省事。

#25楼 2011-02-24 13:55 横刀天笑

@ Todd Wei
1、我想不通为什么一旦集成就会废掉或改掉一大堆单元测试。再说test first也没有说都使用mock编写单元测试来驱动啊。在我们项目中mock测试是不被鼓励的,鼓励编写更多的集成测试,当然,还是test first。

2、如果我快速集成让需求稳定,然后test after,你是什么时候写测试?产品发布后补么?那也就是只要需求不稳定,我就可以拒绝写测试对不?

当然我也不是一定说一定要test first。但是我觉得楼主所说的不是test first的问题。只要你开发软件,你编写测试你就会遇到这些问题。

#26楼 2011-02-24 14:14 Ivony...

@ Todd Wei:
>>>>外部需求可以不稳定,但还是不妨碍我们使用TDD来提高内部质量。这是其一。 ... 但是反馈的环是一环套一环的,越往外的环代价会越大。我们为什么不构建一些内环呢?
>>正因为越往外代价越大,我们才需要尽快进行集成,写内环的单元测试会大大拖慢集成的时间,而一旦集成又会废掉或改掉一大堆单元测试。


第一个问题,越是细粒度的东西,就越不太可能被改变而是直接抛弃,但事实上即使测试和代码一同被抛弃,也只是相对于这个项目而言,一段完成特定功能的测试完成的代码可以直接运用于别的地方。

第二个问题,先写测试很费事,会拖慢进度。这个问题应该这么来看,如果你的项目足够小,那么质量不会成为成为你进度的羁绊。但如果你的项目大到一定程度,模块中的小错误会不断叠加最后使得集成的成本大幅上扬,甚至无法得到一个稳定的可演示的版本。这个时候测试虽然会费事,但综合来看,它节省了总的时间成本。

>>>>需求变了为啥不可以改测试。那么如果采用Test after的方式,需求变了我们就可以不改测试了么
>>TDD才有改不改的问题,如果我快速集成让需求稳定,然后test after,这就不是修改了。可见,test after比test first省事。

这种观点其实还是传统的思维,那事实上我们说,干脆code after不更Happy?先把需求搞清楚再动手,这样代码都不用修改了,可见code after比code first省事。

如果需求最终可以稳定,那么不需要敏捷。敏捷应付的是需求永远无法稳定的场景,这也是现实的场景,人们每时每刻都在产生需求。

当然我们说一个可以工作的软件能更好的来沟通需求。这里的关键在于我们需要的是一个可以工作的软件。一坨未经测试的代码如何证实其是可以工作的呢?如果丢给客户因为各种Bug使得客户无法了解其功能和使用方法,又如何来更好的沟通呢?所以测试是开发过程中必须的,每一个迭代过程中的必须步骤而不是发布前的步骤。

#27楼[楼主] 2011-02-24 14:16 Todd Wei

@ 横刀天笑
>>我想不通为什么一旦集成就会废掉或改掉一大堆单元测试。
两种情况:1.集成后用户看到效果后决定调整需求;2.在集成中发现之前单元测试的假设不成立。

>>如果我快速集成让需求稳定,然后test after,你是什么时候写测试?产品发布后补么?那也就是只要需求不稳定,我就可以拒绝写测试对不?
在第一次集成之前原则上不作内环的单元测试,这是为了保证尽快集成。实际开发中,一般第一次集成发现的问题比较多,对需求的调整比较大,之后会逐渐收敛趋于稳定。一旦集成之后就可以开始根据集成和需求的调整情况做单元测试。

#28楼 2011-02-24 14:19 Ivony...

代码的价值 = 功能 * 测试覆盖。没有测试的代码的价值= 0$。从某个方面来说测试并不能直接创造价值,但能提升代码的价值,确保编码过程是一个创造价值的过程。

代码就像是蒸的馒头,测试就像是卫生许可证,你没有卫生许可证的话,馒头再好吃也不能卖。

#29楼[楼主] 2011-02-24 14:21 Todd Wei

@ Ivony...
>>代码的价值 = 功能 * 测试覆盖
哪位专家提出的?

#30楼[楼主] 2011-02-24 14:23 Todd Wei

@ Ivony...
>>测试先行从根本上确保了开发人员在完成有价值的事情,而不是写一大堆没有实际用处的代码。
恰好相反,测试先行多是基于开发人员自己的假设,是自己认为有价值的事情,而不是用户。集成以后大量作废或需要修改的单元测试就是最好的例子。

#31楼 2011-02-24 14:39 Ivony...

引用 Todd Wei:
@Ivony...
>>测试先行从根本上确保了开发人员在完成有价值的事情,而不是写一大堆没有实际用处的代码。
恰好相反,测试先行多是基于开发人员自己的假设,是自己认为有价值的事情,而不是用户。


并不是最终需求才有价值,你所说的问题与测试先行没有什么关系。

来看一下您现在的模式,开发人员自己虚构一些需求然后完成再交付客户改进。那么很大程度上开发过程中是否创造价值取决于这些虚构的需求是否与最终需求契合。

考虑到虚构的需求和最终需求的巨大差异,我们尽可能的节省虚构需求的开发成本无可厚非。

但在我看来,这根本就不是一个敏捷的过程。


敏捷的开发过程是一个演进的过程,每一次的迭代都是建立在上一次的基础之上,所谓最终需求,是无数次的需求迭代而来。敏捷的过程如果细分下来,会显得十分傻瓜,就像这样:

客户,我想要一个表格。
开发,马上画一个表格。
客户,我想这个表格上有表头,并有这些列
开发,马上加上表头。
客户,我想这些列可以点击排序。
开发,开发排序功能。
客户,我想这些行可以选择。
开发,加上选择功能。
客户,我想编辑或删除这些行。
开发,加上编辑删除功能。

也就是每一次的需求变更都应当是在上一次的基础上,这才叫做演进。换言之,每一次的需求都是正确但不充分的。

反面的教材:

客户:我需要一个表格
开发:给一个包含表头可以点击排序点击选择编辑删除的表格。
客户:我希望这个表格可以包含这些列
开发:去你妈的,老子弄大半个月了,又改,又改!



说回来,如果你觉得测试先行在项目的早期对你只是负担的话,完全可以忽略这个原则。任何模式都有其适用场景,了解原则背后的目的远比遵循原则来的重要。

#32楼 2011-02-24 14:43 横刀天笑

@ Todd Wei
嗯,我的理想工作是,有个客户坐在我旁边(而且是说话能算得上数得客户),然后我在旁边写代码,我要尽快得让我开发得东西能工作,所以我要抛弃一切其他放缓工作的东西,所以我写的每一行代码都是要能在产品中运行的,显然测试代码不是这样的代码,所以我不写了。当我开发出一点东西后,我要马上展示给我旁边的客户看,客户说这样可以,我让她签字,然后我开始编写测试,然后我把代码签入锁定。客户如果说不可以,我就再修改一下,然后再展示给她看。
我就期望我能这么日复一日的开发对最终用户有用的软件。

#33楼 2011-02-24 14:50 Lex Cui

引用 Nick Wang (懒人王):
@Todd Wei

我们的工作到底是什么?
我们的工作不是写代码。
我们的工作是解决问题,是通过编写软件来解决问题。就好像你准备买一跟网线来连接你的电脑和网络,接上后却发现连不通,那么这跟网线解决了你的问题么?显然没有。软件也是一样,如果软件的...

在一个大的Team里,架构师才负责解决问题,而一般程序员只需要照做就可以了。也就是楼上所说的需要称职的民工来做稳定的模块。tdd对他们来说是有必要的。

引用szwe:如果需求允许的话,没有人不希望是瀑布开发吧,不单单是维护文档的问题,还有责任和风险的问题。TDD虽然维护的强度增加了,但是责任和风险也减小了,最起码在开发之前你对很多方面(比如性能,易用性和扩展性)心里有个数了,不会陷入客户无休止的改进要求里面。

维护强度增加难道不意味着进度风险增加吗?

#34楼 2011-02-24 14:52 Lex Cui

引用 横刀天笑:
@Todd Wei
嗯,我的理想工作是,有个客户坐在我旁边(而且是说话能算得上数得客户),然后我在旁边写代码,我要尽快得让我开发得东西能工作,所以我要抛弃一切其他放缓工作的东西,所以我写的每一行代码都是要能在产品中运行的,显然测试代码不是这样的代码,所以我不写了。当我开发出一点东西后,我要马上展示给我旁边的客户看,客户说这样可以,我让她签字,然后我开始编写测试,然后我把代码签入锁定。客户如果说不可以,我就再修改一下,然后再展示给她看。
我就期望我能这么日复一日的开发对最终用户有用的软件。

这个“她”字用到巧妙啊。。。
补充,日复一日这个词也够YD。。。

#35楼 2011-02-24 14:53 横刀天笑

@ Todd Wei
敏捷里有非常多的实践,拿出某一个实践出来单独讨论价值就会损失很多。比如说需求波动很大。这要看看粒度问题。如果你的粒度是整个软件,那么确实需求的波动很大。但是如果在一个合适的粒度上,需求的波动就会降低。比如,我们常常会拿到功能列表后,划分用户故事,对于用户故事这个粒度上需求的波动就会降低,甚至可以不变了。那么在这个粒度上我们为什么不能实施测试驱动开发了呢?
还有,什么东西都是权衡的,有的地方值得你的花费精力自动化测试,而有的地方却不值得,比如你花费很多时间,用了一个超牛得技术能够自动测试界面上得颜色,这个有多大价值?而且这种东西用户改动的频繁程度会很大。而对于一些核心业务的地方(比如,我原来从事石油行业软件开发,里面会有很多石油计算的算法,这个东西变化的频繁程度就小的多,甚至不怎么变化,我们为什么不能开始就写个测试放在那儿,然后编写实现代码呢?)
还有集成的问题,为什么我们一集成就霹雳啪啦出来一堆的问题,如果我们从项目第一天起就能持续不断的自动集成,我想这个问题或许可以解决。

#36楼 2011-02-24 14:56 Lex Cui

引用 横刀天笑:
还有,什么东西都是权衡的,有的地方值得你的花费精力自动化测试,而有的地方却不值得,比如你花费很多时间,用了一个超牛得技术能够自动测试界面上得颜色,这个有多大价值?而且这种东西用户改动的频繁程度会很大。而对于一些核心业务的地方(比如,我原来从事石油行业软件开发,里面会有很多石油计算的算法,这个东西变化的频繁程度就小的多,甚至不怎么变化,我们为什么不能开始就写个测试放在那儿,然后编写实现代码呢?)

严重支持。

#37楼 2011-02-24 14:57 木+头

小虾米四处张望中

#38楼[楼主] 2011-02-24 14:59 Todd Wei

@ 横刀天笑
>>我原来从事石油行业软件开发,里面会有很多石油计算的算法,这个东西变化的频繁程度就小的多,甚至不怎么变化,我们为什么不能开始就写个测试放在那儿,然后编写实现代码呢?

我文中也提到了“TDD在某些需求特别固定的场合是适用的,尤其是与具体业务关系不大的需求,比如:写一个通用的数据结构,实现一个通用算法”。我们金融行业的项目也有很多与具体业务流程关系不大的算法,这些东西很适合TDD。

#39楼 2011-02-24 16:15 FredWang

几点看法:
1.搞的好的TDD一定给力。
2.本人目前做不到完整版的,但较稳定的代码使用TDD(test first),感觉良好。最起码是个码农就能看懂。
3.码农砌代码时,一定会test first,要么写下来,要么在大脑里。
4.把大脑中的test先写下来,有助于整理逻辑,也为日后修改提供保障。基于test的实现代码一定是有价值的。
5.测试既然写下来,就有了维护的成本,以本人的经验来看,修改测试的成本远远高于代码维护的成本。
6.之所以test first的test难修改,因为它和完事之后的test比起来,纯粹原味,更原始,完事之后的test代码,仅仅是补了test代码而已。
7.所以test first难的原因,还是业务收集,分析,抽象的问题。最关键的要点,本人以为是抽象层次和粒度的处理。
8.毫无疑问,本人(应该大部分人)不能做出好的抽象模型。
9.没有必要强求test first出来的test的覆盖率,在团队有信心的并且比较关键的地方,或者类似算法之类输入输出比较确定的地方test first好了;没有信心的,不是很关键的部分,付出巨大代价在test的维护上,价值未必很大,完事之后意思一下,聊胜于无。

#40楼 2011-02-24 16:46 卡通一下

引用 木+头:小虾米四处张望中

在你的周围都是“长须鲸”,看着吓人,但只吃小虾米,哈哈!

#41楼 2011-02-24 16:54 卡通一下

引用 Ivony...:
敏捷的过程如果细分下来,会显得十分傻瓜,就像这样:

客户,我想要一个表格。
开发,马上画一个表格。
客户,我想这个表格上有表头,并有这些列
开发,马上加上表头。
客户,我想这些列可以点击排序。
开发,开发排序功能。
客户,我想这些行可以选择。
开发,加上选择功能。
客户,我想编辑或删除这些行。
开发,加上编辑删除功能。
...

这有点瞎扯了,呵呵!

我至今也搞不清敏捷开发,但现实中的开发绝非是这样,你这样是以你自己对开发的理解,在“自拉自唱”。哈哈...

#42楼 2011-02-24 17:10 木+头

引用 卡通一下:
引用木+头:小虾米四处张望中

在你的周围都是“长须鲸”,看着吓人,但只吃小虾米,哈哈!

在大鲸鱼肚里饱食经验,也是快事一件

#43楼 2011-02-24 17:13 FredWang

引用 卡通一下:
引用Ivony...:
敏捷的过程如果细分下来,会显得十分傻瓜,就像这样:

客户,我想要一个表格。
开发,马上画一个表格。
客户,我想这个表格上有表头,并有这些列
开发,马上加上表头。
客户,我想这些列可以点击排序。
开发,开发排序功能。
客户,我想这些行可以选择。
开发,加上选择功能。
客户,我想编辑或删除这些行。
开发,加上编辑删除功能。
...

这有点瞎扯了,呵呵!

我至今也搞不清敏捷开发,但现实中的开发绝非是这样,你这样是以你自己对开发的理解,在“自拉自唱”。哈哈...


他说的没错啊,小步迭代是敏捷的最重要特征。
不过讲起来简答,干起来太难了。
小步迭代要逐渐从抽象到具体,需求获取和把握,避免在一个迭代中混淆与不同抽象层次,都是很困难的问题。

#44楼 2011-02-24 17:17 卡通一下

@ FredWang

也许你有你的道理,但我是在没有彻底搞清客户的想法前,是绝不会轻易动手的,呵呵!

#45楼 2011-02-24 18:59 助平君

我觉得作为一个还算合格团队,自我判断某种方式(包括TDD)是否对工作有意是很重要的。有用则留,无用则去。

项目的性质不同,所选择的方式必然会不同。如@Todd Wei 与 @横刀天笑 谈到了TDD适合某些和业务关系不大的场合,比如石油行业、医疗器械以及游戏行业,其中有很大一部分的内容是很少变动或者永远不变的,所以由于编写测试的维护成本可以忽略不计。但是测试代码却意义重大,这些变动极少的功能可能调用极为频繁,所以功能的准确性尤为重要。

我之前经历的产品开发过程中,对使用测试并通过持续集成来确保核心算法的正确性,有深刻的体会。

#46楼 2011-02-24 19:15 jnj

“我们真正应该做的是尽快看到实际运行的效果,而测试作为固化的需求和设计是在看到效果之后。”

确实是应该尽快让代码运行并得到结果。但是问题是如何去“看”这个结果?如果你的函数仅仅是返回一个int,你或许可以用print直接打印出来“看”,稍微复杂一些的如:字符串,URL,或者浮点数,“看”起来就费力多了。这个时候我们可能会想到使用assertion,测试本身不就是assertion吗?

“过度的TDD只会导致迟迟看不到实际运行效果,看到效果需要调整需求又会废掉或改掉一大堆的测试用例”

TDD加了过度,也就不是一个好的实践,就像钱多过度也未必是好事。
没有测试用例,需求调整的时候是不是会更轻松些?这个值得商榷。在需求调整的时候,测试用例往往会帮助你发现一些你疏忽掉的应该改动的代码。
删除不再需要的测试用例是必要的,没必要心疼,需求调整和代码重构都会导致一些测试不再有效,测试的作用之一就是让我们不再惧怕需求的调整,鼓励我们去不断优化和重构代码,同时保证功能的正确。如果将写测试当作一种时间上的浪费的话,这个观念不转变,很难在进一步接受TDD了。出来混总是要还的,就算你现在不写,你在某个时候还是要测试,还是要花时间,但是成本可能会大不一样。

#47楼 2011-02-24 19:15 jnj

引用 卡通一下:
@FredWang

也许你有你的道理,但我是在没有彻底搞清客户的想法前,是绝不会轻易动手的,呵呵!


那我估计你可能永远也无法动手了。

#48楼 2011-02-24 20:20 卡通一下

引用 jnj:
引用卡通一下:
@FredWang

也许你有你的道理,但我是在没有彻底搞清客户的想法前,是绝不会轻易动手的,呵呵!

那我估计你可能永远也无法动手了。

多问、多看、多想,要比多动手好得多,只是不要太极端,呵呵!

#49楼 2011-02-24 20:38 菜阿彬

没什么好辩论的,什么都是双刃剑,自然会有高手把TDD这把剑舞得很好(让回报大于成本),而现实中,舞得不好的人总是占大多数。
软件界和任何界都一样,精英总是少数。
个人觉得单元测试的TDD(尤其是编写Domain Model时)很美:快速反馈周期、信心、小步伐、解耦……
有些场合觉得它不美的时候,我就反省:是这把剑不美?还是我这双手不美?

#50楼 2011-02-24 20:53 横刀天笑

对自己有好处就用,没有好处就不用,这么简单的事情,争论什么呢。如果不知道到底怎么样,那就用用看,如果发现不爽那就查查自己原因,如果觉得问题不在自己这儿那就是方法问题,那就不用。有用就用,没用就不用,跟敏捷有啥关系。
不敏捷又如何,敏捷又如何。能成功交付软件就可以了。

#51楼 2011-02-24 22:02 紫色阴影

又见TDD讨论帖 这东西辩来辩去不会有结果的
仁者见仁 觉得好就用 觉得不好不用就是了

#52楼 2011-02-25 09:43 助平君

引用 横刀天笑:
对自己有好处就用,没有好处就不用,这么简单的事情,争论什么呢。如果不知道到底怎么样,那就用用看,如果发现不爽那就查查自己原因,如果觉得问题不在自己这儿那就是方法问题,那就不用。有用就用,没用就不用,跟敏捷有啥关系。
不敏捷又如何,敏捷又如何。能成功交付软件就可以了。

@横刀天笑
这才是正确的方法。就好比在学校,每个人都有自己的学习方式,但是目标就是很简单,考入梦寐以求的学校。那些所谓的学习经验,按部就班的话,还真不一定合适呢。

#53楼[楼主] 2011-02-25 10:06 Todd Wei

@ jnj
>>确实是应该尽快让代码运行并得到结果。但是问题是如何去“看”这个结果?...这个时候我们可能会想到使用assertion,测试本身不就是assertion吗?
取决于你做的东西是在哪个层次的。如果你写的是一个通用的数据结构或算法,那么它的需求相对稳定,单元的需求实际上就是对它的最终需求,尽早地做单元测试没有问题。相反,如果你的工作是一个业务相关的类,从类的需求到设计,在集成之前你所有一切都仅仅是开发人员的假设,是一种未经验证的不成熟的设计。只有等到集成以后才能确定你的设计是否合理,有没有考虑不周全的地方(基本上集成都会发现原先在单元层面考虑不周全,或假设错误)。所以,在集成之前,即使这个类都过了单元测试,到集成的时候也很可能单元测试面临修改或废弃。我并不反对单元测试,而是认为应该先集成后单元测试。

>>TDD加了过度,也就不是一个好的实践,就像钱多过度也未必是好事。
最早极限编程的TDD提倡“没有测试用例失败时不要写一行代码”,这不仅是过度,而且是极端。我提倡的是,一般情况下除了一些需求稳定的通用数据结构、算法,涉及业务的模块在集成之前不要做单元测试。这是我判断是否过度的一个简单标准。原文中我已经修改了用词。

>>没有测试用例,需求调整的时候是不是会更轻松些?这个值得商榷。在需求调整的时候,测试用例往往会帮助你发现一些你疏忽掉的应该改动的代码。..如果将写测试当作一种时间上的浪费的话,这个观念不转变,很难在进一步接受TDD了
当产品相对稳定(尤其是正式发布以后),自动化测试的回归作用是非常重要的,但在开发初期采用TDD是不适合的。能少走的弯路当然要尽量避免,瀑布模型的拥护者也会说“我们应该尽量多花时间先把需求分析做完美,如果将需求分析做完美当成浪费时间的话,这个观念不转变,很难进一步接受瀑布模型”,所以,这不是观念问题,而是现实的优化问题。

#54楼 2011-02-25 10:40 jnj

@ Todd Wei
很喜欢这样的讨论,受益匪浅,为了让讨论更具体,我们在酷壳举了一个具体的小例子,以它来展开讨论,老兄觉得如何?
http://coolshell.cn/articles/3766.html/comment-page-1#comment-32153

#55楼 2011-02-25 11:18 RealDigit

测试用例的造型和需求文档是有十分大的差异的,这个强搬过去只会搞得大家都痛苦

赞同LZ观点

#56楼 2011-02-28 16:56 鱼有胖的头

正轨的项目必须经过严格的测试,才能确保代码的质量,一般都要多人走读代码的,TDD说到底是确保达到这一目标的方法论,这和一个公司所处的国家文化,地域文化,做事理念都有很紧密的联系的。到底美还是不美,看情况。

#57楼23551642012/4/14 17:52:23 2012-04-14 17:52 freecoder

楼主对TDD只知其一不知其二.满篇批判TDD对需求的观点,有点极端.楼主说,有时需求很模糊或者不稳定,所以TDD就不适合了.

我想说的是,需求模糊或者不稳定的项目,才是TDD真正用武之地!
如果是那些需求非常明确的,反而可以不用TDD.比如你做一个图像识别软件,那是研究算法就可以了,用不用TDD反而无所谓.

我个人认为,TDD有3条腿,1、是用例先行。2、单元测试和重构,3、迭代集成。楼主只批判了TDD一条腿,批判的是不完整的TDD。TDD从本质上讲,是分步试错法。因为人不可能一开始就非常清晰软件改怎么做,架构改怎么设计。所以只能慢慢的一步步做,不断尝试错误,不断的完善功能,不断改进设计,直到接近目标。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结