持续集成

持续集成之戏说Check-in Dance

http://www.infoq.com/cn/news/2011/01/ci-check-in-dance

【编者按】众所周知,敏捷软件开发方法中有多种最佳实践,既有管理方面的,也有技术方面的。在尝试敏捷之初,并不是每个团队都能使用全部最佳实践,也不是每个实践都能在短时间内见效。但其中有一种最佳实践却是团队的必选,那就是持续集成,但这并不表示持续集成非常容易。


尽管Thoughtworks的首席科学家Martion folwer 为“持续集成 ”下了定义,但由于自身背景与经历的不同,每个人对其都有不同的理解。从狭义上讲,持续集成可以认为是一种基于某种或者某些变化对软件系统进行的经常性的构建活动(注:这里的构建活动不仅指编译打包工作,还包含各类自动化测试、部署及发布活动)。然而,它忽视了一点,即:任何实践中都应该包含“与人的交互”这一因素。因此,从广意上讲,持续集成应该是软件开发团队在上述活动的约束下所采用的整个开发流程及活动。它强调开发团队与持续集成系统之间的互动性。我们既见过持续集成做得非常成功的团队,也见过效果不佳的持续集成,甚至失败的案例。

那么,到底如何从持续集成中得到最大的收益呢?这要从持续集成所涉及的诸多方面进行分析,并根据团队具体情况(比如团队规模、人员组成以及是否为分布式团队 等)及所开发软件自身的特点(是企业应用软件,还是中间件?是嵌入式软件,还是互联网产品等)制定实践策略与实现步骤。本专栏将与大家共同探讨与持续集成、持续部署及持续交付相关的方法、工具与经验。作者本人在Thoughtworks公司曾参与的一款持续集成与发布管理产品Go的交付和对外咨询服务为专栏提供了很有素材,同时感谢肖鹏 、 李彦辉 、胡凯 、李剑等对栏目内容的支持和帮助。

在软件开发中,持续集成实践能够解决的问题是尽早的集成和尽早的反馈。因此,尽管目前流行的所有版本控制工具都提供了分支/合并功能,但在少于20人的团队中实现持续集成的话,推荐使用Single Branch开发策略。这样会减少多分支开如在合并时的开销。另外,由于理想情况下,每个分支都需要有专属的持续集成环境(包括持续集成服务器、构建环境和测试环境等),所以Single Branch也减少了对持续集成环境的需求量(当编译时间较长或测试用例较多时,这个因素的影响尤其重要)。

当团队完成最初搭建持续集成服务器,编写好一键式编译和测试脚本工作后,就需要考虑如何利用持续集成环境高效地进行团队协作开发了。一定有人会问:

“多人同时在一个分支上开发的话,每个人提交时都要合并代码,不是更浪费时间吗?”

这个问题也正是持续集成期望解决的问题。每当开发人员提交代码时,就是其与其他开发人员工作成果的一次集成。如果每个人都能够频繁提交代码,那么代码集成的频率就会提高,在持续集成的有力支持下,代码中潜在的问题就会更早地暴露出来(比如代码编译链接问题,自动化测试失败反映出来的代码功能问题,或需求理解不一致等问题),以便团队尽早解决之。

当然,持续集成所鼓励的频繁提交并不是指那种仅将版本控制库当成备份工具,无约束的“随意”提交,还需要团队开发流程约束的。下面我们来一同探讨“持续集成环境中的团队开发流程是什么样的”。

让我们先设想一个软件开发场景。

一、使用版本管理工具做备份

故事的主人公叫Joe,他打算写一个游戏,所以用Subversion建立了一个版本控制库用于保存代码,然后就动手写代码了。Joe的开发流程是这样的。

  1. 从代码库中检出一份代码;
  2. 为增加某个功能修改一些代码;
  3. 在本地运行了一下自动化测试;
  4. 测试通过之后,提交代码到版本控制库;
  5. 重复前面的步骤。

如图1所示。

二、搭建持续集成服务器做自动构建

“每次在本地手工运行自动化测试太麻烦了,”Joe想到,“这种重复的工作为什么不让机器来做呢”。

于是,Joe上网查了一下,发现持续集成工具是做这个事情的,就找来一台旧机器,用CruiseControl搭建了一个持续集成服务器。他的开发流程也变为:

  1. 从代码库中检出一份代码;
  2. 开发新功能或修改bug;
  3. 提交到版本控制库,思考下一个功能的实现;
  4. 持续集成服务器运行自动化构建和测试;
  5. 如果测试通过,转到步骤(1);
  6. 如果测试没有通过,转到步骤(2)。如图2所示。

三、多人并行开发

两周后,游戏初见原型,Joe向他的几个朋友介绍了他的游戏创建,他们都非常喜欢,因此也加入了游戏开发。麻烦很快就出现了。持续集成服务器中构建结果经常失败,所以每次检出代码后都要做问题清理工作。于是,Job与朋友们坐下来讨论如何解决这个问题。

Alice说:“我们每个人都拉一个独立分支,当每个人的功能开发完成以后,再合并到一起不就行了吗?”

Joe不同意这样的做法。“游戏的需求还不明晰,要经常合在一起看一下效果。所以还是在同一个分支上开发吧。下面,我们讨论一下如何让这种失败少一些吧。”

于是,他们花了点儿时间,发现有两个主要原因导致失败。

  1. 本地代码有问题,原本就编译不了或会导致测试失败,但还是提交了;
  2. 开始做新功能时,没有特别注意分支上的持续集成状态,直接将主分支上的代码直接就与本地代码合并了;

Joe提出,开发流程应该变成如图3所示:

  1. 每个人在开发新代码之前,只能从持续集成已成功的那个最新版本检出代码;
  2. 开发新功能或修改bug;
  3. 提交前将主分支上的代码再次取到本地合并;
  4. 运行本地测试,确保测试可以通过;
  5. 提交代码到主分支,由持续集成服务器再次运行测试。
  6. 如果测试通过,转到步骤(1);
  7. 如果测试没有通过,转到步骤(2),直到修复持续集成上的构建。

可是,Alice提出反对意见。她认为:“既然本地已经运行了测试,为什么还要在持续集成服务器上再次运行呢?”

Joe解释到:“主要是因为我们每个人的本地环境都不完全相同,很可能出现‘它在我的机器没有问题呀’的这个现象,所以还是要在独立的持续集成服务器上再运行一次。”

因此,大家就这么决定了。

四、两次本地构建的目的

四周后的一天,Joe花了很长时间完成了某个新功能后,打算提交了。于是他把分支当前的代码与其本地代码进行了一次合并。然后运行了本地测试,但测试失败了。他用了很长时间来定位该问题是在他自己修改的功能里,还是在被合入的代码中。这让他对提交流程进行了反思。

“要是在合入他人代码之前,能够先运行一次本地测试,验证一下我的代码没问题就好了,反正本地测试所花的时间也不长。”

于是,他把这个想法告诉了其他人,最后大部分人都同意这么做。于是,其提交流程就变成了这样:?

  1. 每个人在开发新代码之前,只能从持续集成完全成功的那个最新版本检出代码;
  2. 开发新功能或修改bug;
  3. 运行本地测试,如果有失败就立即修复,直至测试成本;
  4. 提交前将主分支上的代码再次取到本地合并;
  5. 运行本地测试,确保测试可以通过;
  6. 提交代码到主分支,由持续集成服务器再次运行测试。
  7. 如果测试通过,转到步骤(1);
  8. 如果测试没有通过,转到步骤(2)。

这个过程就被称为“Check-in Dance”。

Alice还说道:“我们在从主分支上检出代码时,一定是那个通过持续集成验证的最新版本。这样可以避免检出的代码就是有问题的,而影响自己本地的代码。”整个过程如图4所示。

五、持续集成令牌

过了几天,有人把大家叫到了一起,这次是Alice。她说:

“我今天遇到一个问题。我提交代码之后,正等着持续集成服务器返回结果呢,Bob就提交代码了。幸好我提交的代码通过了测试,否则的话,我就要在Bob的代码之上修复啦。所以,我建议我们需要设立一个提交令牌,只有拿到这个提交令牌的人才能提交。也就是说,当一个人做完本地测试之后,去拿这个令牌。拿到之后,再进行代码合并、本地测试和提交。提交以后当持续集成服务器返回成功通过的结果时,才能交还令牌。这样就不会出现我和Bob这种情况了。”

可Bob并不同意这样的做法,“这次没有出什么问题,为什么还要这么做呢?”

此时,Joe把话接了过来,说道:“Alice的这个建议很好,我已经遇上过一次这样的事情了,那次测试失败以后,我花了很长时间才发现问题并不在我的提交中,而是在Mary的提交中。我把它修复后,又做了一次提交。”由于大多数人都同意这么做,因此团队决定试一试。因为目前测试运行时间很短,所以提交和集成工作没有遇到什么瓶颈。提交流程如图5所示。

似乎事情到这里就结束了。然而,这个游戏被某投资公司看中,决定做更大的投入,招更多的开发人员,让它成为一个开放游戏平台。那么,接下来Joe与他的朋友们还会遇到哪些问题呢?

 

持续集成之“测试三角形与分段构建策略原则”

http://www.infoq.com/cn/news/2011/02/ci-test-triangle

随着软件产品新特性的不断增加,软件自动化测试用例的数量也会成倍增长。对于一些历史“悠久”的遗留系统来说,甚至会积累数以万计的自动化测试用例。如果对这样的系统进行持续集成,还要求每个开发人员都要进行本地验证的话,困难的确不小。让我们还是看看Joe的团队是如何解决类似问题的吧。

在《戏说Checkin Dance》一文 中,咱们说到:Joe?的团队实施了带有令牌的持续集成提交流程纪律。由于每个人都做本地构建进行验证后再提交,所以持续集成平台上的构建结果比较稳定,每天持续集成服务器上的构建最多只有 一两次失败(常见的原因是忘记提交某个文件而导致失败,和因本地环境配置与平台环境配置不一致而导致失败),但一般都能在30分钟内修复。随着项目的进 行,新功能不断地增加,自动化测试用例也越积越多。由于不做任何修改,本地构建脚本就会运行所有自动化测试用例,所以本地构建的运行时间也越来越长。团队里有人开始抱怨,“每次提交代码前,运行本地构建都超过15分钟,这样太浪费时间。我们可否把那些不太重要的测试拿出去,不再运行了?”

一、自动化测试黄金三角形

作为团队的技术负责人,Joe把大家叫到一起,就这个问题进行了专门的讨论。

“我们不能放弃运行这些测试。”Alice说道,“在我前一个项目中,我们就是这么做的,结果,这些花精力写的测试都作废了。”

“那是为什么呢?”?Bob问道。

Alice回答道:“因为并不经常运行这些测试,随着功能的修改,有些测试的逻辑就不再是正确的了。而当再次运行发现这类问题时,通常的结果就是把这个测试删掉,因为修复这个测试的工作量太大了。”

“那持续运行所有测试的话,等待的时间太长了,也是一种浪费呀。?”Bob说道。

此时,作为团队技术负责人的Joe说话了。“让我们先分析一下,到底有哪些什么原因让我们的测试在这么短的时间里就变成需要这么长时间了呢?”

“功能增加的多了,测试自然就多了呗。”

“功能增加了,自动化测试数据的准备工作也多了,需要的时间当然就长了。”

“现在我们的测试中有很多地方需要测试在原地等待结果返回,所以等待时间也挺长的。”

“大家还有没有其它原因?”Joe追问道。

大家沉默了一会儿,Bob说道:“好象主要就这些原因吧”。

“那好吧。功能多而导致测试多这是好事儿,说明我们大家都非常重视我们的自动化测试。对于‘测试准备时间变长’这个问题可以理解,因为我们的产品越来越复杂了。对于‘结果返回的等待问题‘嘛,需要具体问题,具体分析。前几天,我看到一个‘测试黄金三角形’,讲的就是自动化测试中各类测试的应具有的比例关系,对我很有启发。我在白板上画一下吧。”于是,Joe走到白板前,将这个测试黄金三角形画了下来,如图1所示。

然后,Joe将这个图形解释了一下。原来,这个三角形讲的就是单元测试、集成测试和验收测试的关系。首先,左边向上的箭头表示,越高层次的测试维护成本越高,运行时间越长。因此,对于单个测试来说,单元测试运行最快,维护最容易,而集成测试次之,验收测试则最高。?每类测试的面积代表着该测试的数量。现在,业界有很多种工具支持单元测试,因此它的编写及维护成本相对其它两种测试来说较低,应使用单元测试对代码做尽可能多的测试覆盖。一般来说,单元测试覆盖率达到70~80%是比较理想的状态。

接着,Joe问了大家一个问题:“我们产品中的这些自动化测试属于哪一类测试?”

Alice说道:“那要看你怎么定义单元测试中的这个单元。”

“根据WikiPedia上的定义,一个单元是指应用程序中最小可测试的部分。既然我们使用面向对象的开发语言C++,那么单元测试的粒度应该是类中的一个方法吧。而且,通常来说,如果一个测试包括以下任何一个情形,它就不是一个单元测试:(1)需要连接数据库;(2)需要网络通信;(3)需要与文件系统打交道;(4)不能和其它单元测试同时运行;(5)需要对环境进行一些配置(如编辑配置文件)才能运行它。”Joe回答道。

“要是这么说的话,我们的测试中,一部分是模块集成测试,一部分是验收测试,只有一小部分算是单元测试。我们的测试集合正好是一个倒三角。”Bob边说,边在白板上画了出来,如图2所示。

“既然高层次上的测试(集成测试和验收测试)维护量比较大,今后我们应该加入更多的低层次测试(单元测试),对于关键功能进行集成测试和验收测试。如果对于测试用例具有等价性的话,我们应该用低层次测试来实现。这样我们就会达到自动化测试的黄金三角状态啦。”Joe边说边在白板上笔划着,如图3所示。

“我同意你说法,但是仍旧没有解决我们目前遇到的问题。如何解决我们现在本地构建时间太长的问题呢?”Alice有点儿不耐烦地问道。

二、分阶段构建?

“这还不容易,Martin Folwer(敏捷宣言的创造者之一)已经给出了一个解决方案,那就是两阶段构建(Secondary Build)。也就是说,我们可以把那些运行比较慢,时间比较长且基本上不会失败的自动化测试用例挑选出来,组成一个新的测试集,在第二阶段运行,可以叫做‘二级构建阶段’。剩余的测试集仍旧放在第一个阶段运行,我们可以把第一个阶段叫做‘提交构建阶段’。”Joe回答道。

“那什么时间运行这两个阶段的构建呢?”Bob问道。

“提交阶段构建当然就是在我们每个人提交之后就运行啦。而且在我们提交之前,作为本地验证集合,在我们开发环境上也要运行同样的提交构建。一般来说,本地构建和提交构建最好都在五分钟内完成,最长也不要超过十分钟,否则开发人员就不愿意花时间做频繁地代码提交啦。另外,一旦提交阶段构建成功以后,就马上自动触 发第二阶段构建。而我们开发人员在持续集成服务器上的提交阶段构建成功以后,就可以继续进行其它的工作啦。”Joe说道,“我们原来的六步提交图就变成这 个样子了。”说着,Joe拿起白板笔就画了出来,如图4所示。

“不对,这里有问题!持续集成强调尽早反馈。如果把测试分成两个阶段了,那反馈周期不是加长了 吗?”Bob反驳道。

Joe 点点头,说道:“你说的没有错。但是,根据我们现有的软硬件资源条件,我们目前还无法通过增加资源的方式来缩短所有测试运行的时间。所以我们必须在质量与速度之前做出平衡。这也是我为什么要把那些不易出错的自动化测试集合放在第二阶段构建的原因,这样可以降低但不能完全解除第二阶段构建失败的风险。所以, 这也要求我们大家当第二阶段构建失败时,也要找人尽快把它解决,并且把相关的测试再次放回提交测试阶段中运行,或者在提交测试阶段加入新的测试来补充。” ?

Alice此时插话,问道:“既然第二阶段构建不常失败,为什么我们不定时运行它,比如每天晚上运行一次呢?这样不是更节省资源吗?另外,如果第二阶段构建运行得慢,那它不是一直都落后吗?”

“因为每次提交阶段构建成功以后就触发第二阶段构建,这样无论如何都比每天晚上运行一次的更多的反馈。因为每天晚上运行一次的话,如果出了问题,我们只能在第二天早上才能发现。对于你的第二个问题,我画一张图来解释。”Joe找了一张大白纸,在上面开始画了起来。

一会儿功夫,几个示意图就画好了。看到这几个示意图以后,大家恍然大悟。如图5所示。从图中我们可以看到:

  1. 当版本123的第二阶段构建被触发并正在运行,Alice又提交了一次,触发了版本124的提交构建;
  2. 当版本124的提交构建完成之后,由于版本123的第二阶段构建仍在运行,所以不再触发第二阶段构建;
  3. 当版本125的提交构建完成时,版本123的第二阶段构建仍旧在运行,所以也不触发第二阶段构建;
  4. 当版本126提交构建正在运行时,版本123的第二阶段构建刚完成,此时由于版本125的提交阶段构建是一个最近 成功完成的提交构建,所以持续集成服务器就会启动该版本的第二阶段构建,而忽略版本124的提交构建。

“那根据我们持续集成纪律,谁的提交让构建失败,就由谁来修复。如果版本125的第二阶段构建失败了,就包括版本124和125两次提交的变更,由谁来修复呢??”Bob接着问道。

“这个好办,由这两个提交人一起负责修复。如果想确切找到谁的提交有问题,还可以手动触发版本124的第二次构建。假如构建成功,说明版本125有问题,假如构建失败,说明问题在版本124就引入了。”Alice抢着说道。

讨论到这里,团队成员都达成了共识,(1)开始加强单元测试的力度;(2)在反馈速度和反馈质量之间做出折衷,使用二级构建构建的方式。

整个产品的开发非常顺利,马上就要进行版本发布了。团队还会遇到什么问题呢?他们是如何解决的呢?请听下回分解。?

posted @ 2014-05-29 21:59  ooxiaofangoo  阅读(326)  评论(0编辑  收藏  举报