<td id="cl7yg"></td>

        <code id="cl7yg"></code>

          天极传媒
          天极网
          比特网
          IT专家网
          52PK游戏网
          极客修
          全国分站

          北京上海广州深港南京福建沈阳成都杭州西安长春重庆大庆?#25103;?/a>惠州青岛郑州泰州厦门淄博天津无锡哈尔滨

          产品
          • 网页
          • 产品
          • 图片
          • 报价
          • 下载
          全高清?#38431;?#26426; 净化器 4K电视曲面电视小?#19994;?/A>滚筒洗衣机
          您现在的位置 天极网 > 开发>新闻>测试驱动开发TDD介绍中的误区

          测试驱动开发TDD介绍中的误区

          博客 2014-04-10 10:57 我要吐槽

          目前我正在教授一个为期两周的敏捷开发实践速成课参加培训的团队成员都是非常传统的企业级Java开发者要将社区中15年的进展浓缩到8个半天的实践课程中非常具有挑战性在严格地时间约束下教授什么思想和实践才能对这些开发者的职业生涯提供最大帮助呢

          经过几天断断续续的思考我至少得出了一个结论传?#25104;?#20250;介绍给新人的测试驱动开发(TDD)将?#25442;?#20986;现在我的课程里

          通常的TDD介绍存在着本质性的问题它们通常会将学习者置于一个通往貌似目的地的道路上但事实上并不能展示如何到达真正的目的地这种现象太常见了所以我决定给它取个名?#37073;?#29616;在该咋搞朋友(WTF now, guys?)

          Fig. 1 说真的到底什么情况

          我认为这?#26234;?#20917;解释了为什么开发者对TDD的看法存在如此多分歧当某个开发者发出这样的抱怨到处都是mock对象太可怕了?#20445;?#21478;一个正在?#23454;?#26356;高山峰的开发者可能会回复啊到处都是Mock对象多好呀!事实上这是人们谈论TDD时出现的典型情景并且我相信出现这种问题的原因是我们用相同的词汇和工具描述完全不相关实践一个对于山峰前边的人?#27492;岛?#21512;理的TDD问题对于正在探索山峰另一边的人来说可能完全是荒谬的

          如果我是对的(读完下文后你可以?#32422;?#21028;断)我认为这个观点揭示了为什么很多开发者曾经对TDD的许?#23707;统?#27493;使用经验感到如此兴奋最终却开?#20960;?#21040;失望

          通过code katas(编码实践)教授经典TDD

          我们先来看看如?#38382;?#29992;code katas教授TDD

          首先我会简单演示一下如何通过测试驱动出一个返回?#25105;?#26000;波那契数的函数我会不断对?#32422;?#35828;这一整天的例子尽管不那么实用但至少可以用?#27492;得?#32418;灯-绿灯-重构的开发节奏?#34180;?#31245;后我们会过一下Bob大叔的保龄球计分kata当天培训的最后是由参加者?#32422;?#32467;对实现一个罗马数字到阿拉伯数组的转换函数

          第二天我会站在白板前让同学们总结一下他们所体会的TDD的?#20040;?#26159;什么不出意外(但这点很重要)所有学员都将TDD看作是跟正确性相关的代码没有缺陷?#34180;?#33258;动化回归测试代替手动测试?#20445;?#20462;改代码不用担心会改坏原有功能等?#21462;?/P>

          当我对他们的回答评论说TDD的主要?#20040;?#26159;提高我们的代码设计!?#20445;?#20182;们显得有些猝不及防并?#19994;?#25105;告诉他们TDD所带来的任?#20301;?#24402;测试安全性往好了说只是副作用往坏了说可能只是个幻觉他们开始左顾右盼希望确保他们的?#20064;?#27809;有听到我所说的这听起来可不像是他们最初希望得到的东西

          假设换种方式我只是提供编码练习就像我每天所做的一样而忽略它们只是些简单的练习题这一事实当学生们发现他们在TDD编程练习中学到的经验对平时的工作毫无帮助时他们会有多么失望

          错误#1鼓励庞大的代码单元

          对初学者来说如果你的目标是让每个测试都对解决你的问题有直接帮助那么最后你就会得到功能越来越多的代码单元第一个测试将会得到一些直接解决问题的程序代码第二个测试会带来更多第三个测试会让你的设计更加复杂TDD实践本身?#25442;?#22312;任何时候告诉你需要改进实?#30452;?#36523;的设计将大段的代码分割成小段

          Fig. 2 考虑上图如果出现一个新需求大多数开发者都会想到在现有的单元上增加额外的复?#26377;ԣ?#32780;?#25442;?#39044;先想到新需求需要通过增加一个新的单元来实现

          防止代码设计变成一团?#34915;?#21464;成了留给开发者的练习这就是为什么很多TDD支持者要求在测试通过后增加一个繁重的重构步骤?#20445;?#22240;为他们意识到需要在这个流程中对开发者进行?#31245;员?#35753;他们能够停一下发现简化设计的机会

          ?#30475;?#27979;试通过后进行重构是TDD支持者的原则(毕竟要遵守红灯-绿灯-重构)不过在实践中很多开发者经常错误地的跳过这个步骤因为TDD过程没有任何内在的规定强迫人们重构直到最后代码变成一团?#34915;?/P>

          一些培训者会告诫开发者严格的重构才能体现纪律性与专业性的美德希望以此来解决这个问题这对我来说这并不能算是个解决方案与其去质疑那些做出巨大努力练习TDD的人们的职业素养我宁愿去质疑在工具和练习的设计上是否能够鼓励人们在工作流中做正确的事

          错误2#鼓励费力的重构提取操作

          假设在代码单元开始变?#38376;?#22823;时你会主动进行提取的重构操作

          Fig. 3 将单元的一部分职责提取到一个新的子单元中不改变原始的测试以确保我们的重构没有?#33529;?#20219;何东西

          不过需要知道提取重构通常都会很痛苦提取重构通常需要仔细的分析?#32422;?#20840;神贯注这样才能将一个复杂的父对象梳理为一个整洁的子对象和一个不那么复杂的父对象引用Brandon Keeper所说的把两个毛线团打成一个节比把一个打了节的毛线团分成两个毛线团要容易得多?#34180;?/P>

          错误3# 正确代码的特性测试

          ?#35789;?#37325;构工作顺利完成了还有很多工作要做!为了保证系统中每个单元?#21152;?#23545;应的设计良好的单元测试(我称它为对称测试)你需要设计新的单元测试来描述新的子对象行为这种做法很有问题因为特性测试是处理遗留代码的测试工具在真正的测试驱动开发中根本不应当出现同时如果我们将特性测试定义为为没有测试的单元添加测试以验证其行为?#20445;?#36825;正是在描述我们所做的为已经实现的没有相应单元测试的单元编写测试

          因为新的测试并不是用正常的TDD节奏编写的开发人员面临着与实现后添加测试情况同样的风险也就是说因为代码已经存在了你的特性测试无法确保验证到了新的子单元的全部行为所以?#35789;?#20320;为了覆盖新的单元做了这么多额外的(也是值得称赞的)工作能达到的测试质量上限也始终比从头进行测试驱动开发要低这个结果?#24471;?#20102;这种活动其实是种浪费

          Fig. 4 为新的子单元行为添加特性测试我们需要对测试的健壮性持谨慎的态度因为它是开发后添加测试的产物

          错误4# 冗余的测试覆盖

          但是现在你的系统面临着另一个测试陷阱冗余的测试覆盖!同一行为在两个地方都被覆盖到对于TDD新手来说可能感觉很舒适直到改动成本开始变得失控

          假设来了一个新的需求要求改动提取出来的子对象行为理想情况下这需要做三处改变(这三点是开发者都能够预测到的)验证新特性的集成测试描述新行为的单元测试?#32422;?#21333;元代码本身但是在我们的冗余测试例子中父单元的测试同样需要做出修改

          更糟糕的是实现这个变更的开发者根本想不到父对象的单元测试会失败也就是说最好的情况是开发者面临一个意想不到的惊喜?#20445;?#29238;单元的测试失败了需要额外的精力根据子对象的行为去重新设计父单元的测试最差的情况可能是开发者可能没有意识到这个测试失败其实是一个由于业务改变而导致的误报并不是一个真实的bug这会导致大量时间耗费在发现父单元测试的失败原因上

          Fig. 5 子对象的修改导致父对象的测试失败需要重新设计父对象的测试?#35789;?#29238;对象本身并没有修改

          假设子对象被用在两个地方甚至10个地方!一个被依赖单元的简单修改就会导致对依赖单元数小时的痛苦测试修复工作

          错误5# 以牺牲回归有效性来消除冗余

          如果我们希望避免冗余测试最终所带来的痛苦那么实现一个简单的提取方法的重构就要求我们重新设计父单元的测试

          要知道父单元的测试原本是有正确性和回归安全?#21592;?#35777;的所以原来的作者可能并不?#19981;?#25105;为了移除冗余所做的事把父单元中的子单元实例替换成它们的测试替身

          Fig. 6 将父单元测试由原来的使用真实子单元实例替换成测试替身

          现在这些测试就没什么意义了它们实际上验证不了任?#38382;?最初的作者可能会这么说根据当初编写这些代码的本意来说(TDD就是迭代式的解决问题同时保证了完全的回归安全)他们的意见是绝对正确的可以这样反驳他们的观点可是这些单元已经有独立的测试了?#20445;?#20294;是因为缺少额外的集成测试确保这些单元协同工作的正确性原作者的担心并不是没有道理的

          在这一点上我见多过很多团队进入死胡同一些人会很?#19981;?#20351;用mock另一些人则十分反对mock但是没有人真正理解这种争论只是一?#30452;?#35937;它的根源是经典TDD给我们提供的错误假设

          错误6# 滥用Mock

          虽然我通常会推荐团队使用mock但他们像如下这样使用mock并不是个好主意首先将子单元替换成测试替身将会导致父单元的测试复杂化测试代码的一部分会表述父单元的逻辑行为另一部分则会描述预期中的父单元与子单元协作方式除了要处理以上的两方面的内容测试还绑定了父子单元如何协作的细节因为任何调用都必须与父单元的实现逻辑相配套

          像这样即描述了逻辑行为?#32622;?#36848;了单元协作过程的测试是很难阅读理解与修改的并且这种恐怖的情况可能遍及使用测试替身的大多数测试之中这就难怪我总是听到单元测试里有太多mock的抱怨最近这个问题也很让我困扰

          要解决这种滥用问题工作量也很大父单元需要做重构让它只是引?#35745;?#20182;单元的协作而本身不含有任何实现逻辑这就要求父单元里那些之前没有提取到子单元中的行为现在需要被抽取到另一个新的单元中(包括目前为止讨论到的所有耗时的活动)最终父单元原始的测试将被抛弃新的测试将只包含关于协作的描述确保各子单元之间的?#25442;?#26159;必须的哦由于现在完全没有集成测试确保父单元工作正常我们还需要添加一个集成测试

          天那使用这套方法需要这么多精力?#32422;?#32426;律性才能维护一套整洁的代码可理解的测试?#32422;把?#36895;的构建这就难怪很少有团队最终达到使用TDD所希望达成的目标

          成功应用TDD的方式

          因此我希望提供一个全新的课程引入与上文描述完全不同的TDD工作流

          首先考虑一下上文所述的痛苦曲折过程的最终产物

          一个父单元依赖两个子单元实现逻辑功能

          父单元的测试描述了两个子单元的?#25442;?/P>

          两个子单元每个?#21152;?#21333;元测试描述他们各自的职责

          如果这就是我们要达到的最终目标为什么不在最开始?#32479;?#30528;这个方向前进我的TDD方法考虑到了这一点并且可以做为简化论的一个应用

          我的流程是这样的

          (1) 拉入一个新的特性需求这要求系统完成一些新的功能

          (2) 为特性看起来的复?#26377;?#24863;到?#21482;Q?#24605;考?#32422;?#20026;什么一开?#20960;?#19978;了编程这份工作

          (3) 为特性?#19994;?#19968;个切入点从建立一个公?#27493;?#21475;契约开始(例如我会为控制器增加一个行为返回给定年月的利润值)

          此时也是将公共契约写入集成测试的好机会本文并不是关于集成测试的不过我推荐运行于?#32422;?#29420;立进程的测试它可以像真实用户那样与应?#23186;换?例如通过HTTP请求)如果从一开始就加入集成测试保证回归安全性我们的单元测试?#31570;?#38656;要太多考虑集成测试的问题了

          (4) 为切入点编写单元测试不过不需要尝试立刻去直接解决问题要有意识地延迟编写实现逻辑!应当这样假想你已经有了所需要的一些对象通过这种方式来化简问题(例如如果这个控制器只依赖一个根据?#36335;?#33719;取收入的对象和一个根据?#36335;?#33719;取开支的对象那一定很简单)

          由于这个步骤本身就鼓励使用小型单功能的单元因此它能改善你的设计

          (5) 用TDD的方式实?#26234;?#20837;点代码编写测试就像那些假想的单元已经存在了在切入点要用到的依赖对象处注入测试替身在测试中描述它和依赖之间的?#25442;换?#27979;试描述那些协作单元它们只负责控制其他单元的使用本身不包含逻辑

          这一步能够改善你的设计因为它给你机会去发?#20013;乱?#36182;所必须拥有的API如果某个?#25442;?#24456;难测试那么修改函数签名会很容的因为这些依?#30340;?#21069;还没有实现

          (6) 对每个新想到的对象重复步骤4和5发现更多更小粒度的协作对象

          受人类天性影响人们在这一步时可能会感到比较?#21482;?这样我们会得到无穷多微小的类!)但在实践中通过很好的代码组织这是可以管理的由于每个对象都很小容易理解用途单一因此通常由于需求变化而要删除某个不再使用的单元或是对象体系中的一整个子树的对象并?#25442;?#24102;?#26149;?#22823;痛苦(我曾经遇到过 一份很不幸的代码里边有很多庞大的被到处使用的对象因而基本上不可能删除它们?#35789;?#23427;们已经不再符合当初的设计初衷了)

          (7) 最终直到工作再无法细分此时在这些对象图中处于叶子节点的对象中实现最后的一点逻辑之后重新回到树的顶端开始下一个修改

          这个过程的目标就是发现尽可能多的协作对象这样就可?#21592;?#35777;叶子节点只需要实现最简单的逻辑

          逻辑单元的测试会详尽得描述有效行为并且可以让作者有理?#19978;?#20449;单元测试是完备与正确的逻辑单元的测试可?#21592;?#25345;简单因为没有必要使用测试替身只需要根据不同的输入验证对应的输出结果

          我?#19981;?#25226;这种过程成为Fake it until you make it(使用伪对象直到你实现它)虽然这其实是来自GOOS一书中的敏锐观点但它更强调?#24605;?#32422;化我还发现区分协作单元与逻辑单元是很有价值的不但能够使测试更清晰而且也增加了代码的一致性

          同时注意到采用这种TDD方式不需要加入繁重的重构步骤提取重构操作成为一种特例而不是常规行为这就意味着上?#21335;该?#36848;的提取重构带来的后续附加成本能够完全避免

          改变我们教授TDD的方式

          我用了四年时间才完全理解我使用TDD所遇到的挫折并将这些思考写成了这篇文章经过对这些问题长时间的徘徊与思索我可以说最终我认为TDD是一个高效的愉快的实践不值得为所有的项目尝试投入那么多TDD时间但在构建一个预期会生存很长时间的系统时TDD是能够帮助我们战胜焦虑和复?#26377;?#30340;一个有效工具

          我将这些分享给大家的目的是告诉大家这才是真正的测试驱动开发经典TDD的简单假设给新人带来的痛苦并不能让他们学?#25945;?#22810;东西让我们一起?#19994;?#19968;种方法能够将更有效的TDD工作流教授给学员让他们可以立刻使用这些有效的工具将令人困惑的大问题拆分成为可以控制的小问题

          原文链接 testdouble 翻译 伯乐在线 - 治不好你我?#31570;?#26159;兽医

          译文链接 http://blog.jobbole.com/64431/

          作者伯乐在线责任编辑?#21644;?#29577;平
          请关注天极网天极新?#25945;?nbsp;最酷科技资讯
          扫码赢大奖
          评论
          * 网友发言均非本站立场本站不在评论栏推荐任何网店经销商谨防上当受骗
          办公软件IT新闻整机
          ҹʱʱ

          <td id="cl7yg"></td>

              <code id="cl7yg"></code>

                <td id="cl7yg"></td>

                    <code id="cl7yg"></code>