文档首页 > > 理论实践> 持续开发与集成>

结对编程,你开车,我导航

结对编程,你开车,我导航

分享
更新时间:2021/02/27 GMT+08:00

通用的测试驱动开发基本过程

  1. 明确当前要完成的功能。可以记录成一个TO DO(待办)列表。
  2. 快速完成针对一个功能的测试用例编写。
  3. 测试代码编译通过,但测试用例通不过。
  4. 编写对应的功能代码。
  5. 测试通过。
  6. 对代码进行重构,并保证测试通过。
  7. 循环完成所有功能的开发。

乍一看,似乎也没什么。但深奥之处就在于第一步的明确上。如何明确?通常由业务、测试、开发进行一次讨论,就要完成的功能的验收条件达成一致并形成记录,然后测试人员设计并编写验收测试用例,开发人员编写单元测试和并实现功能代码。这样,测试人员早期介入,从而可以避免开发人员与测试人员理解不一致,产生争执并阻塞等待业务分析人员或者行政主管的仲裁。

对于开发人员来讲,可以强迫他从测试的角度来考虑设计,考虑代码,这样才能写出适合于测试的代码。

从另外一个角度上说,坚持测试优先的实践,可以让开发人员从一个外部接口和客户端的角度来考虑问题,从而保证软件系统各个模块之间能够较好地连接在一起;而开发人员的思考方式,也会逐步地从单纯的考虑实现,转移到对软件结构的思考上来。这才是测试优先的真正思路。

重构是XP里面非常重要的一个实践,只有不断地重构,才能改善代码质量、提高代码复用,它跟TDD/简单增量设计是相辅相成的,谁都离不开谁。

在TDD中,除去编写测试用例和实现测试用例之外的所有工作都是重构。所以,没有重构,任何设计都不能实现。至于什么时候重构嘛,还要分开看,我的经验是,实现测试用例时重构代码,完成某个特性时重构设计,产品的重构完成后还要记得重构一下测试用例。

重构不可避免地会带来一些问题,我们需要建立一个很好的机制保障重构的正确性。其中很重要的一个实践就是单元测试。虽然一些简单的重构可以在没有单元测试的情形下进行,重构工具与编译器自身也提供有一定的安全保障,但如果只采用传统方式对代码进行测试,例如使用调试器或执行功能测试,这种测试方法不仅效率低下,而且是乏味的、不值得信赖的。重构时,代码较以前对修改更为敏感与脆弱。若要避免不必要的问题,则应添加单元测试到项目中。这样可以确保每一小步的重构,都能够及时发现错误。

  • 如果一段代码还能工作,没有出现问题,就不要动它?

    如果一个系统一直没有新的需求,使用的情形一直不变,这样做是可以的。但对于95% 的产品而言,是需要不断变化的。如果一些冗余代码、拙劣的代码,存在糟糕的结构和投机性设计,虽然能够正常运行,但这样的软件,常常会带来更大的潜在问题。对于一个负责任的程序员来讲,是不能容忍的。一定要重构,重新优化,夺回对代码的控制权, 千万不能滋生得过且过的思想!

  • 通过TDD就可以发现很多Bug了?

    按照TDD的方式进行的软件开发可以有效地预防Bug,但不可能通过TDD找到Bug。因为TDD里有一个很重要的概念是“完工时完工”。意思是说,当开发人员写完功能代码,通过测试了,工作也就做完了。当开发人员的代码完成的时候,即使所有的测试用例都亮了绿灯,这时隐藏在代码中的Bug一个都不会露出马脚来。即使之前没通过测试,那也不叫Bug,因为工作还没做完。所以还需要我们测试人员同步设计功能测试用例,进行功能验收测试才行。那个阶段发现的问题才能真正称为Bug。

  • 我该为一个功能特性编写测试用例还是为一个类编写测试用例?

    关于TDD的文章大多都说应该为一个功能特性编写相应的Test Case(测试用例)。后来看了一篇博客文章,才明白是怎么回事。在开发一个新特性时,先针对特性编写测试用例,如果发现这个特性无法用测试用例表达,那么将这个特性细分,直至可以为手上的特性写出测试用例为止。然后不断地重构代码,不断地重构测试用例,不断地依据TDD的思想往下做,最后当产品伴随测试用例集一起发布的时候,他们发现经过重构以后的测试用例,就已经和产品中的类/方法一一对应啦!

  • TDD到底该做到什么程度,才算结束了呢?重构总是无止境的。是通过所有的UT测试用例吗?

    “Clean Code That Works.”这句话是TDD的目标,Work是指代码奏效,也就是必须通过所有的UT测试用例, 而Clean是指代码整洁。前者是把事情做对,后者是把事情做好。

关于结对编程

说到结对,大家通常都会立即想到编程结对,其实在XP中,这个概念可以更宽泛一些,还可以是设计结对、评审结对、单元测试结对。

设计结对是在对某个模块开始编码之前,两人共同完成该模块的设计,这种设计通常不会花费很长时间,不会产生设计文档,更多的是讨论交流,主要考虑是否符合总体架构,是否足够灵活,易于重构等。

单元测试结对通常是说一个人编写测试代码,另外一个人编写代码来满足测试。这样,任何一个人对设计理解有误,代码都无法通过单元测试,从而避免由同一个人编写单元测试代码和程序代码带来的黑洞,往往可以发现更多的问题或缺陷。

评审结对是在编码活动完成、通过单元测试后进行的。一般采用一个人讲述代码组织和编程思路,一个人倾听、提问的形式。这种评审模式更多地强调了相互交流,这会比一个人单独评审,独立撰写总结评审意见的模式效率要高得多,文档、邮件也减少了。也许有人说,这么做就会没有文档化的评审记录。可谁会关心这个呢?良好的代码应该说明了一切。

其实,如果两人编程结对了,编程的过程其实也就是复审的过程,完全可以省略评审。

设计结对、评审结对、单元测试结对这三种方式是对结对编程实践的有效补充,操作简单,收益却很大。
  • 编程结对,在任一时刻都只是一个程序员在编程,效率到底有多高呢? 1+1>1是肯定了,但是否1+1>2呢?

    现在还没有肯定的答案!国外也有很多关于结对编程的研究,基本都是建立在结对的两人组和一个人之间的对比,结论基本上是结对编程不能始终保证开发质量和效率始终高于单人编程。如果是结对的两人组和两个单人开发组进行对比,结果更是未必。

    只有两个经验相等的人结对才有可能真正提高编码效率。而现实中,经常是一个有经验的人坐在旁边,另一个经验不太丰富的人进行编程,还会有一个老手轮询多个新手进行开发的方式,在国内公司尤其普遍,这样就更难做到1+1>2。

    通常支持结对编程的人认为,当两个人合作三个月以后,效率才有可能超过两个人单独编程的效率!这里有一个时间前提,三个月以后。三个月这个时间未必是真实确凿的时间分界线,它只是一个模糊的、大概的时间范畴,如果两个人配合得好,也许只需要两个多月,如果配合不好,也许是四、五个月或者更长的时间,不确定性很大。

    此外,结对编程始终是两个人的合作行为,其效果会受到多种因素影响。譬如,两个人的性格、个人关系、沟通能力、技术是否互补等都会影响最终的结果。究竟1+1大于2还是小于2真的是一个很难说的事情。只能靠团队自己不断地组合,找出合适的配对人选。

    其实不仅仅编程结对,其他结对实践,也要视人、视项目、视环境而定。至少两个极端情形下,结对毫无益处:第一, 需要静心思考的问题。这时完全可以分头行动,等各自有了理解或解决方案再来讨论;第二,琐碎毫无技术含量的工作,不得不手工完成的。这种工作考验的只是耐心,不妨分头行动,效率肯定比结对要高。

    在有些时候还是可以采用的,特别是对于新加入一个团队的成员而言,可以让他迅速成长,融入团队!因为结对编程的内涵是一种技术、经验、知识的共享,通过共同商讨、解决问题,来降低误解和疏远。但即使是这样的结对,一天中最好也不要超过3小时。

结对编程的要点

  1. TDD以可验证的方式迫使开发人员将质量内建在思维中,长期的测试先行将历练开发人员思维的质量,而事后的单元测试只是惶恐的跟随者。
  2. 重构不是一种构建软件的工具,不是一种设计软件的模式,也不是一个软件开发过程中的环节,正确理解重构的人应该把重构看成书写代码的方式或习惯,重构时时刻刻有可能发生。
  3. 软件构建学问中总有一些理论很美好,但是一经使用就可能面目全非,比如传统的瀑布模型。敏捷里有很多被称之为思想的东西,恰恰没有太高深的理论,但都是一些实践的艺术,强调动手做而不是用理论论证。TDD就是这样一种东西,单纯去研究它的理论,分析它的优点和缺点没有任何意义,因为它本身就是一个很单纯的东西,再对其抽象也得不出像"相对论"那样深奥的理论。实践会给出正确的答案。
  4. 结对编程不是一种形式化的组合,在实际的XP小组中,结对的双方应该是根据需要不断变换的,应该保证双方都是对这部分工作感兴趣的人,而不是强行指定。
  5. 结对编程不是结队编程,是两个人,不是更多。可以扩展到结对设计、结对测试、 结对评审。
  6. 就像Scrum一样,并不是所有的团队都有能力实行XP,也不是所有的团队都适合实行XP,视实际情况而定。
  7. XP中,多数实践方法是互相加强甚至是互相保证的,不能单单拿出某一个实践来单独实施,譬如结对编程,缺乏TDD/重构/简单递增设计等实践的有效补充,结对编程的效果可能会大打折扣。

每日构建与持续集成

每日构建有诸多好处,相比传统滞后的集成,已经是前进了一大步。敏捷转型从每日构建开始,是很明智的选择,下一步就是持续集成。

每日构建与持续集成的区别在于“频率和反馈两个方面”;每日构建的问题在于,第一天晚上的构建如果失败,第二天还要提交新的代码,然后再等到晚上进行构建,到第三天才能知道结果。如果构建的状态总是失败,团队会习惯于这样失败的状态,长此以往,很容易就恢复到之前的延迟集成的状态。

此外,根据Jez Humble《持续交付》一书中的建议,“下班之前,构建必须处于可工作状态”,这个是有别于每日构建的。有几个原因,第一,持续集成强调的是及时反馈,如果提交代码,构建失败,等到第二天回来再进行修复,记忆已经没有那么清晰了;第二,今日事今日毕,敏捷强调节奏和可工作的软件;第三,如果存在跨时区的团队,一旦构建出现问题,且相关人员已经下班,则对方团队的正常工作就会受到影响。

提交代码触发持续集成,应该留出可能的修复时间。但预留多少时间好呢?太短可能不够修复的,太长又浪费。《持续交付》书中的另外两条规则"时刻准备着回滚到上一个版本"与"在回退之前要规定一个修复时间",就是针对这种情况。规定一个修复时间,在此时间之前如果还未能恢复持续集成服务器的正常,就进行版本回退,然后进行一次构建和冒烟测试,通过以后才可以下班回家。

持续集成的理念是:如果经常对代码库进行集成对我们有好处,为什么不随时做集成呢?随时的意思是每当有人提交代码到版本库时。

测试驱动开发TDD

Kent Beck说:“当遇到TDD的问题,从来都不是TDD的问题”,是设计的问题,是测试的目标太大。TDD是工程师治愈焦虑的一种方式,分解问题,试验并获得反馈的一种方式。

测试也是一个学习的方式和过程。测试是反馈,因此,测试速度要快,TDD是一个非常漂亮的短循环。

TDD也是一种教育的方式。TDD可以帮助新人快速通过试错取得成长,是一个开发人员积累经验的过程。

代码评审

Kent Beck说:“有Code Review比没有Code Review好,但是没有Code Review比有Code Review好”;这样类似绕口令的一段话,很有意思。代码评审会造成等待、延迟以及工作切换,但有代码评审总比没有好,虽然你不能指望代码评审发掘太多问题。比代码评审更好的,是通过协作的方式来保障质量,比如结对编程,此时,Code Review的作用就没有那么明显。

也有人说,Code Review的重要性在于社会性意义,当你知道有人会检查你的代码时, 你会更认真。对此,我也认同。但是,当知道有人会检查你的代码,可能产生两种心态, 一种是正面的,你会更具责任心,一种是负面的,你可能会产生依赖心理。

测试人员对于开发质量也是如此,当知道有专职的测试时,自己少做一个测试,也总会有人在后面把关。

在脸书,所有代码都在公司范围公开,由不同的人维护,每个人都有修改权限,可以push(推送)到不同的代码主干和分支上,并可以部署到生产环境。一个修改,几亿人会使用,成就感极大,压力感会更大,由此转化成了责任感。当开发人员全权负责, 没有测试会帮你检查,没有其他人可以依赖,权力越大责任越大,最终的结果却很好。因为这种自由信任的氛围,因为鼓励尝试,允许失败的文化。

关于代码风格等评审,工程师不应该过多地关注编程风格的事情,这些可以使用工具来帮助,这类的小错让机器来解决。工程师应该把精力投入到软件设计和实现这些重要的事情上,允许人来犯更大的错误(做更勇敢的尝试)。

结对编程,是信息沟通与知识传递的过程;两个人的水平,无论是高高,还是高低的搭配, 搭配1~2个月,两个人的水平都会提高;此外,如大民所说,虽然结对编程两个人的输出,不会比两个人分别做更高;但是质量会大幅提升,因此带来的整体成本下降; 结对是关键,所以未必是编程,大民也提到结对的设计、测试和评审,最关键的是两人互补做出决策,类似于“四眼”原则;结对的角色不必相同,事实上不同角色的结对, 更有利于全栈工程师的培养,以及技术的共享;除了设计、开发、测试,建议结对的范围,把运维也拉进来,这样就真正地实现了DevOps里讲的开发与运维的协作。

本文内容节选自《敏捷无敌之DevOps时代》,作者:王立杰、许舟平、姚冬(清华大学出版社)。

分享:

    相关文档

    相关产品