UML软件工程组织

持续集成
作者:Martin Fowler & Matthew Foemmel

    译者语:2002123日,我们很荣幸的在UMLCHINA组织的网上交流中聆听了Martin Fowler先生的教诲。在交流中,Martin Fowler向所有中国软件开发者推荐了这篇文章:Continuous Integration(《持续集成》)。初读之下,我便感觉到了它的分量,AgileChina的林星也称赞:“其中的思想非常的好,大师就是大师。”然后,用了一周的时间,我终于把这篇文章翻译出来,以飨读者。

    由于这是Fowler先生送给全体中国软件开发者的礼物,所以我绝对不敢独占。任何人都可以在任何地方随意转载本文,但是在转载时请保持本文完整性——包括标题、版权声明、原文链接、译者语……总之,请不要在转载的时候做任何改动或增删。另外,如果能在转载的时候顺手给我一个mail,我会更加高兴。

    下面,请开始欣赏这篇精彩的文章。

 

    在任何软件开发过程中都有一个重要的部分:得到可靠的软件创建(build)版本。尽管知道创建的重要性,但是我们仍然会经常因为创建失败而惊讶不已。在这篇文章里,我们将讨论MattMatthew Foemmel)在ThoughtWorks的一个重要项目中实施的过程,这个过程在我们的公司里日益受到重视。它强调完全自动化的、可重复的创建过程,其中包括每天运行多次的自动化测试。它让开发者可以每天进行系统集成,从而减少了集成中的问题。

ThoughtWorks公司已经开放了CruiseControl软件的源代码,这是一个自动化持续集成的工具。此外,我们还提供CruiseControlAnt和持续集成方面的顾问服务。如果需要更多的信息,请与Josh Mackenziejmackenz@ThoughtWorks.com)联系。

本文有以下主要内容:

    在软件开发的领域里有各种各样的“最佳实践”,它们经常被人们谈起,但是似乎很少有真正得到实现的。这些实践最基本、最有价值的就是:都有一个完全自动化的创建、测试过程,让开发团队可以每天多次创建他们的软件。“日创建”也是人们经常讨论的一个观点,McConnell在他的《快速软件开发》中将日创建作为一个最佳实践来推荐,同时日创建也是微软很出名的一项开发方法。但是,我们更支持XP社群的观点:日创建只是最低要求。一个完全自动化的过程让你可以每天完成多次创建,这是可以做到的,也是完全值得的。

    在这里,我们使用了“持续集成(Continuous Integration)”这个术语,这个术语来自于XP(极限编程)的一个实践。但是我们认为:这个实践早就存在,并且很多并没有考虑XP的人也在使用着它。只不过我们一直用XP作为软件开发过程的标准,XP也对我们的术语和实践产生了深远的影响。尽管如此,你还是可以只使用持续集成,而不必使用XP的任何其他部分——实际上,我们认为:对于任何切实可行的软件开发活动,持续集成都是很基本的组成部分。

    实现自动化日创建需要做以下几部分的工作:

  • 将所有的源代码保存在单一的地点,让所有人都能从这里获取最新的源代码(以及以前的版本)。
  • 使创建过程完全自动化,让任何人都可以只输入一条命令就完成系统的创建。
  • 使测试完全自动化,让任何人都可以只输入一条命令就运行一套完整的系统测试。
  • 确保所有人都可以得到最新、最好的可执行文件。

    所有这些都必须得到制度的保证。我们发现,向一个项目中引入这些制度需要耗费相当大的精力。但是,我们也发现,一旦制度建立起来,保持它的正常运转就不需要花多少力气了。

持续集成的优点

    描述持续集成最大的难点在于:它从根本上改变了整个开发模式。如果没有在持续集成的实践环境中工作过,你很难理解它的开发模式。实际上,在单独工作的时候,绝大多数人都能感觉到这种气氛——因为他们只需要与自己的系统相集成。对于许多人来说,“团队开发”这个词总让他们想起软件工程领域中的一些难题。持续集成减少了这些难题的数量,代之以一定的制度。

    持续集成最基本的优点就是:它完全避免了开发者们的“除虫会议”——以前开发者们经常需要开这样的会,因为某个人在工作的时候踩进了别人的领域、影响了别人的代码,而被影响的人还不知道发生了什么,于是bug就出现了。这种bug是最难查的,因为问题不是出在某一个人的领域里,而是出在两个人的交流上面。随着时间的推移,问题会逐渐恶化。通常,在集成阶段出现的bug早在几周甚至几个月之前就已经存在了。结果,开发者需要在集成阶段耗费大量的时间和精力来寻找这些bug的根源。

    如果使用持续集成,这样的bug绝大多数都可以在引入的同一天就被发现。而且,由于一天之中发生变动的部分并不多,所以可以很快找到出错的位置。如果找不到bug究竟在哪里,你也可以不把这些讨厌的代码集成到产品中去。所以,即使在最坏的情况下,你也只是不添加引起bug的特性而已。(当然,可能你对新特性的要求胜过了对bug的憎恨,不过至少你可以多一种选择。)

    到现在为止,持续集成还不能保证你抓到所有集成时出现的bug。持续集成的排错能力取决于测试技术,众所周知,测试无法证明已经找到了所有的错误。关键是在于:持续集成可以及时抓到足够多的bug,这就已经值回它的开销了。

    所以,持续集成可以减少集成阶段“捉虫”消耗的时间,从而最终提高生产力。尽管现在还不知道是否有人对这种方法进行过科学研究,但是作为一种实践性的方法,很明显它是相当有效的。持续集成可以大幅减少耗费在“集成地狱”中的时间,实际上,它可以把地狱变成小菜一碟。

集成越频繁,效果越好

    持续集成有一个与直觉相悖的基本要点:经常性的集成比很少集成要好。对于持续集成的实践者来说,这是很自然的;但是对于从未实践过持续集成的人来说,这是与直观印象相矛盾的。

    如果你的集成不是经常进行的(少于每天一次),那么集成就是一件痛苦的事情,会耗费你大量的时间与精力。我们经常听见有人说:“在一个大型的项目中,不能应用日创建”,实际上这是一种十分愚蠢的观点。

    不过,还是有很多项目实践着持续集成。在一个五十人的团队、二十万行代码的项目中,我们每天要集成二十多次。微软在上千万行代码的项目中仍然坚持日创建。

    持续集成之所以可行,原因在于集成的工作量是与两次集成间隔时间的平方成正比的。尽管我们还没有具体的衡量数据,但是可以大概估计出来:每周集成一次所需的工作量绝对不是每天集成的5倍,而是大约25倍。所以,如果集成让你感到痛苦,也许就说明你应该更频繁地进行集成。如果方法正确,更频繁的集成应该能减少你的痛苦,让你节约大量时间。

    持续集成的关键是自动化。绝大多数的集成都可以而且应该自动完成。读取源代码、编译、连接、测试,这些都可以自动完成。最后,你应该得到一条简单的信息,告诉你这次创建是否成功:“yes”或“no”。如果成功,本次集成到此为止;如果失败,你应该可以很简单地撤消最后一次的修改,回到前一次成功的创建。在整个创建过程中,完全不需要你动脑子。

    如果有了这样一套自动化过程,你随便想多频繁进行创建都可以。唯一的局限性就是创建过程本身也会消耗一定的时间。(译注:不过与捉虫所需的时间比起来,这点时间是微不足道的。

一次成功的创建是什么样的?

    有一件重要的事需要确定:怎样的创建才算是成功的?看上去很简单,但是如此简单的事情有时却会变得一团糟,这是值得注意的。有一次,Martin Fowler去检查一个项目。他问这个项目是否执行日创建,得到了肯定的回答。幸亏Ron Jeffries也在场,他又提了一个问题:“你们如何处理创建错误?”回答是:“我们给相关的人发一个e-mail。”实际上,这个项目已经好几个月没有得到成功的创建了。这不是日创建,这只是日创建的尝试。

    对于下列“成功创建”的标准,我们还是相当自信的:

  • 所有最新的源代码都被配置管理系统验证合格
  • 所有文件都通过重新编译
  • 得到的目标文件(在我们这里就是Javaclass文件)都通过连接,得到可执行文件
  • 系统开始运行,针对系统的测试套件(在我们这里大概有150个测试类)开始运行
  • 如果所有的步骤都没有错误、没有人为干涉,所有的测试也都通过了,我们就得到了一个成功的创建

    绝大多数人都认为“编译+连接=创建”。至少我们认为:创建还应该包括启动应用程序、针对应用程序运行简单测试(McConnell称之为“冒烟测试”:打开开关让软件运行,看它是否会“冒烟”)。运行更详尽的测试集可以大大提高持续集成的价值,所以我们会首选更详尽的测试。

单一代码源

    为了实现每日集成,任何开发者都需要能够很容易地获取全部最新的源代码。以前,如果要做一次集成,我们就必须跑遍整个开发中心,询问每一个程序员有没有新的代码,然后把这些新代码拷贝过来,再找到合适的插入位置……没有什么比这更糟糕的了。

    办法很简单。任何人都应该可以带一台干净的机器过来,连上局域网,然后用一条命令就得到所有的源文件,马上开始系统的创建。

    最简单的解决方案就是:用一套配置管理(源代码控制)系统作为所有代码的来源。配置管理系统通常都设计有网络功能,并且带有让开发者轻松获取源代码的工具。而且,它们还提供版本管理工具,这样你可以很轻松地找到文件以前的版本。成本就更不成问题了,CVS就是一套出色的开放源代码的配置管理工具。

    所有的源文件都应该保存在配置管理系统中。我说的这个“所有”常常比人们想到的还要多,它还包括创建脚本、属性文件、数据库调度DLL、安装脚本、以及在一台干净的机器上开始创建所需的其他一切东西。经常都能看到这样的情况:代码得到了控制,但是其他一些重要的文件却找不到了。

    尽量确保所有的东西都保存在配置管理系统的同一棵代码源树中。有时候为了得到不同的组件,人们会使用配置管理系统中不同的项目。这带来的麻烦就是:人们不得不记住哪个组件的哪个版本使用了其他组件的哪些版本。在某些情况下,你必须将代码源分开,但是这种情况出现的几率比你想象的要小得多。你可以在从一棵代码源树创建多个组件,上面那些问题可以通过创建脚本来解决,而不必改变存储结构。

自动化创建脚本

    如果你编写的是一个小程序,只有十几个文件,那么应用程序的创建可能只是一行命令的事:javac *.java。更大的项目就需要更多的创建工作:你可能把文件放在许多目录里面,需要确保得到的目标代码都在适当的位置;除了编译,可能还有连接的步骤;你可能还从别的文件中生成了代码,在编译之前需要先生成;测试也需要自动运行。

    大规模的创建经常会耗费一些时间,如果只做了一点小小的改动,当然你不会希望重新做所有这些步骤。所以好的创建工具会自动分析需要改变的部分,常见的方法就是检查源文件和目标文件的修改日期,只有当源文件的修改日期迟于目标文件时,才会重新编译。于是,文件之间的依赖就需要一点技巧了:如果一个目标文件发生了变化,那么只有那些依赖它的目标文件才会重新编译。编译器可能会处理这类事情,也可能不会。

    取决于自己的需要,你可以选择不同的创建类型:你创建的系统可以有测试代码,也可以没有,甚至还可以选择不同的测试集;一些组件可以单独创建。创建脚本应该让你可以根据不同的情况选择不同的创建目标。

    你输入一行简单的命令之后,帮你挑起这副重担常常是脚本。你使用的可能是shell脚本,也可能是更复杂的脚本语言(例如PerlPython)。但是很快你就会发现一个专门设计的创建环境是很有用的,例如Unix下的make工具。

    在我们的Java开发中,我们很快就发现需要一个更复杂的解决方案。Matt用了相当多的时间开发了一个用于企业级Java开发的创建工具,叫做Jinx。但是,最近我们已经转而使用开放源代码的创建工具Anthttp://jakarta.apache.org/ant/index.html)。Ant的设计与Jinx非常相似,也支持Java文件编译和Jar封装。同时,编写Ant的扩展也很容易,这让我们可以在创建过程中完成更多的任务。

    许多人都使用IDE,绝大多数的IDE中都包含了创建管理的功能。但是,这些文件都依赖于特定的IDE,而且经常比较脆弱,而且还需要在IDE中才能工作。IDE的用户可以建立自己的项目文件,并且在自己的单独开发中使用它们。但是我们的主创建过程用Ant建立,并且在一台使用Ant的服务器上运行。

自测试的代码

    只让程序通过编译还是远远不够的。尽管强类型语言的编译器可以指出许多问题,但是即使成功通过了编译,程序中仍然可能留下很多错误。为了帮助跟踪这些错误,我们非常强调自动化测试——这也是XP提倡的另一个实践。

    XP将测试分为两类:单元测试和容纳测试(也叫功能测试)。单元测试是由开发者自己编写的,通常只测试一个类或一小组类。容纳测试通常是由客户或外部的测试组在开发者的帮助下编写的,对整个系统进行端到端的测试。这两种测试我们都会用到,并且尽量提高测试的自动化程度。

    作为创建的一部分,我们需要运行一组被称为“BVT”(Build Verification Tests,创建确认测试)的测试。BVT中所有的测试都必须通过,然后我们才能宣布得到了一个成功的创建。所有XP风格的单元测试都属于BVT。由于本文是关于创建过程的,所以我们所说的“测试”基本上都是指BVT。请记住,除了BVT之外,还有一条测试线存在(译注:指功能测试),所以不要把BVT和整体测试、QA等混为一谈。实际上,我们的QA小组根本不会看到没有通过BVT的代码,因为他们只对成功的创建进行测试。

    有一条基本的原则:在编写代码的同时,开发者也应该编写相应的测试。完成任务之后,他们不但要归还(check in)产品代码,而且还要归还这些代码的测试。这也跟XP的“测试第一”的编程风格很相似:在编写完相应的测试、并看到测试失败之前,你不应该编写任何代码。所以,如果想给系统添加新特性,你首先应该编写一个测试。只有当新的特性已经实现了以后,这个测试才可能通过。然后,你的工作就是让这个测试能够通过。

    我们用Java编写这些测试,与开发使用同样的语言,所以编写测试与编写代码没有太大的区别。我们使用JUnithttp://www.junit.org/)来作为组织、编写测试的框架。JUnit是一个简单的框架,让我们可以快速编写测试、将测试组织为套件、并以交互或批处理的模式来运行测试套件。(JUnitxUnit家族的Java版本——xUnit包括了几乎所有语言的测试框架。)

    在编写软件的过程中,在每一次的编译之后,开发者通常都会运行一部分单元测试。这实际上提高了开发者的工作效率,因为这些单元测试可以帮助你发现代码中的逻辑错误。然后,你就没必要去调试查错,只需要注意最后一次运行测试之后修改的代码就行了。这个修改的范围应该很小,所以寻找bug也就容易多了。

    并非所有的人都严格遵循XP“测试第一”的风格,但是在第一时间编写测试的好处是显而易见的。它们不但让每个人的工作效率更高,而且由这些测试构成的BVT更能捕捉到系统中的错误。因为BVT每天要运行好几次,所以BVT检查出的任何问题都是比较容易改正的,原因很简单:我们只做了相当小范围的修改,所以我们可以在这个范围内寻找bug。在修改过的一小块代码中排错当然比跟踪整个系统来排错要有效多了。

    当然,你不能指望测试帮你找到所有的问题。就象人们常说的:测试不能证明系统中不存在错误。但是,尽善尽美不是我们唯一的要求。不够完美的测试只要经常运行,也比永远写不出来的“完美测试”要好得多。

    另一个相关的问题就是:开发者们为自己的代码编写测试。我们经常听人说:开发者不应该测试自己的代码,因为他们很容易忽视自己工作中的错误。尽管这也是事实,但是自测试过程需要快速将测试转入代码基础中。这种快速转换的价值超过独立测试者的价值。所以,我们还是用开发者自己编写的测试来构造BVT,但是仍然有独立编写的容纳测试。

    自测试另一个很重要的部分就是它通过反馈——XP的一项核心价值——来提高测试的质量。这里的反馈来自于从BVT中逃脱的bug。自测试的规则是:除非你在BVT中加入了相应的测试,否则就不能修正任何错误。这样,每当要修正某个错误的时候,你都必须添加相应的测试,以确保BVT不会再把错误放过去。而且,这个测试应该引导你去考虑更多的测试、编写更多的测试来加强BVT

主创建

    创建过程的自动化对于单个开发者来说很有意义,但是它真正发光的,还是在整个系统的主创建(master build)的生成。我们发现,主创建过程能让整个团队走到一起来,让他们及早发现集成中的问题。

    第一步是要选择运行主创建的机器。我们选择了一台叫做“投石车”的计算机(我们经常玩“帝国时代”J),这是一台装有四个CPU的服务器,非常适合专门用来做创建。(由于完整的创建需要相当长的时间,所以这种马力是必须的。)

    创建进程是在一个随时保持运行的Java类中进行的。如果没有创建任务,创建进程就一直循环等待,每过几分钟去检查一下代码仓库。如果在最后的创建之后没有人归还任何代码,进程就继续等待。如果代码仓库中有了新的代码,就开始创建。

    创建的第一阶段是完全提取仓库中的代码。Starteam已经为我们提供了相当好的Java API,所以切入代码仓库也很容易。守护进程(daemon)会观察五分钟以前的仓库,看最近五分钟里面有没有人归还了代码。如果有,守护进程就会考虑等五分钟再提取代码(以免在别人归还代码的过程中提取)。

    守护进程将全部代码提取到投石机的一个目录中。提取完成之后,守护进程就会在这个目录里调用Ant脚本。然后,Ant会接管整个创建过程,对所有源代码做一次完整的创建。Ant脚本会负责整个编译过程,并把得到的class文件放进六个jar包里,发布到EJB服务器上。

    Ant完成了编译和发布的工作之后,创建守护进程就会在EJB服务器上开始运行新的jar,同时开始运行BVT测试套件。如果所有的测试都能正常运行通过,我们就得到了一个成功的创建。然后创建守护进程就会回到Starteam,将所有提取出的源代码标记上创建号。然后,守护进程会观察创建过程中是否还有人归还了代码。如果有,就再开始一次创建;如果没有,守护进程就回到它的循环中,等待下一次的归还。

    创建结束之后,创建守护进程会给所有向最新一次创建归还了代码的开发者发一个e-mail,汇报创建的情况。如果把创建留在代码归还之后去做,而又不用e-mail向开发者通报创建的情况,我们通常认为这是不好的组织形式。

    守护进程将所有的步骤都写在XML格式的日志文件里面。投石车上会运行一个servlet,允许任何人通过它检查日志,以观察创建的状态。(见图1

    屏幕上会显示出创建是否正在运行、开始运行的时间。在左边有所有创建的历史记录,成功的、失败的都记录在案。点击其中的某一条记录,就会显示出这次创建的详细信息:编译是否通过、测试的结果、发生了哪些变化……

    我们发现很多开发者都经常看看这个页面,因为它让他们看到项目发展的方向,看到随着人们不断归还代码而发生的变化。有时我们也会在这个页面上放一些其他的项目新闻,但是需要把握好尺度。

    要让开发者能在自己的本地机器上模拟主创建过程,这是很重要的。这样,如果集成错误出现了,开发者可以在自己的机器上研究、调试,而不必真的执行主创建过程。而且,开发者也可以在归还代码之前先在本地执行创建,从而降低了主创建失败的可能性。

    这里有一个比较重要的问题:主创建应该是干净的创建(完全从源代码开始)还是增量创建?增量创建会快得多,但是也增大了引入错误的风险,因为有些部分是没有编译的。而且我们还有无法重新创建的风险。我们的创建速度相当快(20万行代码约15分钟),所以我们乐于每次都做干净的创建。但是,有些团队喜欢在大多数时候做增量创建,但是当那些奇怪的问题突然出现时,也经常性地做干净的创建(至少每天一次)。

代码归还(Check in

    使用自动化创建就意味着开发者应该遵循某种节奏来开发软件,最重要的就是他们应该经常集成。我们曾经见过一些组织,他们也做日创建,但是其中的开发者却不经常归还代码。如果开发者几周才归还一次代码,那么日创建又有什么意义呢?我们遵循的原则是:每个开发者至少每天要归还一次代码。

    在开始新的任务之前,开发者应该首先与配置管理系统同步。也就是说,他们应该首先更新本地机器上的源代码。在旧的代码基础上编写代码,这只会带来麻烦和混乱。

    然后,开发者要随时保持文件的更新。开发者可以在一段任务完成之后将代码集成到整个系统中,也可以在任务的中途集成,但是在集成的时候必须保证所有的测试都能通过。

    集成的第一步是要再次使开发者的本地文件与代码仓库同步。代码仓库中所有新近有改动的文件都要拷贝到开发者的工作目录中来,当文件发生冲突时,配置管理系统会向开发者提出警告。然后,开发者需要对同步后的工作集进行创建,对这些文件运行BVT,并得到正确的结果。

    现在,开发者可以把新的文件提交到代码仓库中。提交完成之后,开发者就需要等待主创建。如果主创建成功,那么这次归还也是成功的。如果主创建失败了,开发者可以在本地修改。如果修改很简单,就可以直接提交;如果修改比较复杂,开发者就需要放弃这次修改,重新同步自己的工作目录,然后继续在本地开发、调试,然后再次提交。

    某些系统强制要求归还进程逐个进行。在这种情况下,系统中会有一个创建令牌,同一时间只有一个开发者能拿到令牌。开发者获取创建令牌,再次同步文件,提交修改,然后释放令牌。这就确保创建过程中,最多只能有一个开发者在更新代码仓库。不过我们发现,即使没有创建令牌,我们也很少遇到麻烦,所以我们也不用这种方法。经常会有多个人同时向同一个主创建提交代码的情况,但是这很少造成创建失败,而且这样的错误也很容易修复。

    同时,我们还让开发者自己来决定归还过程中的小心程度。这反映出开发者对集成错误出现几率的评估。如果她觉得很有可能出现集成错误,那么她就会在归还之前先做一次本地创建;如果她觉得根本不可能出现集成错误,那么她可以直接归还。如果犯了错误,在主创建运行时她立刻就会发现,然后她就必须放弃自己的修改,找到出错的地方。如果错误很容易发现、很容易修补,那么这种错误也是可以接受的。

总结

    发展一个制度严密的自动化创建过程对于项目的控制是很重要的。许多软件先贤都这样说,但是我们发现,这样的过程在软件开发领域中仍然罕见。

    关键是要让所有的事情都完全自动化,并且要经常进行集成,这样才能尽快发现错误。然后,人们可以随时修改需要修改的东西,因为他们知道:如果他们做的修改引起了集成错误,那也是很容易发现和修补的。一旦获得了这些利益,你会发现自己再也无法放下它们。


 

 

版权所有:UML软件工程组织