持续集成
原文链接:http://martinfowler.com/articles/continuousIntegration.html
中译本下载:http://gigix.topcool.net/download/ContinuousIntegration.pdf
译者语:2002年1月23日,我们很荣幸的在UMLCHINA组织的网上交流中聆听了Martin Fowler先生的教诲。在交流中,Martin Fowler向所有中国软件开发者推荐了这篇文章:Continuous Integration(《持续集成》)。初读之下,我便感觉到了它的分量,AgileChina的林星也称赞:“其中的思想非常的好,大师就是大师。”然后,用了一周的时间,我终于把这篇文章翻译出来,以飨读者。
由于这是Fowler先生送给全体中国软件开发者的礼物,所以我绝对不敢独占。任何人都可以在任何地方随意转载本文,但是在转载时请保持本文完整性——包括标题、版权声明、原文链接、译者语……总之,请不要在转载的时候做任何改动或增删。另外,如果能在转载的时候顺手给我一个mail,我会更加高兴。
下面,请开始欣赏这篇精彩的文章。
在任何软件开发过程中都有一个重要的部分:得到可靠的软件创建(build)版本。尽管知道创建的重要性,但是我们仍然会经常因为创建失败而惊讶不已。在这篇文章里,我们将讨论Matt(Matthew Foemmel)在ThoughtWorks的一个重要项目中实施的过程,这个过程在我们的公司里日益受到重视。它强调完全自动化的、可重复的创建过程,其中包括每天运行多次的自动化测试。它让开发者可以每天进行系统集成,从而减少了集成中的问题。
ThoughtWorks公司已经开放了CruiseControl软件的源代码,这是一个自动化持续集成的工具。此外,我们还提供CruiseControl、Ant和持续集成方面的顾问服务。如果需要更多的信息,请与Josh Mackenzie([email protected])联系。
本文有以下主要内容:
* 持续集成的优点
* 集成越频繁,效果越好
* 一次成功的创建是什么样的?
* 单一代码源
* 自动化创建脚本
* 自测试的代码
* 主创建
* 代码归还
* 总结
在软件开发的领域里有各种各样的“最佳实践”,它们经常被人们谈起,但是似乎很少有真正得到实现的。这些实践最基本、最有价值的就是:都有一个完全自动化的创建、测试过程,让开发团队可以每天多次创建他们的软件。“日创建”也是人们经常讨论的一个观点,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。”实际上,这个项目已经好几个月没有得到成功的创建了。这不是日创建,这只是日创建的尝试。
对于下列“成功创建”的标准,我们还是相当自信的:
* 所有最新的源代码都被配置管理系统验证合格
* 所有文件都通过重新编译
* 得到的目标文件(在我们这里就是Java的class文件)都通过连接,得到可执行文件
* 系统开始运行,针对系统的测试套件(在我们这里大概有150个测试类)开始运行
* 如果所有的步骤都没有错误、没有人为干涉,所有的测试也都通过了,我们就得到了一个成功的创建
绝大多数人都认为“编译+连接=创建”。至少我们认为:创建还应该包括启动应用程序、针对应用程序运行简单测试(McConnell称之为“冒烟测试”:打开开关让软件运行,看它是否会“冒烟”)。运行更详尽的测试集可以大大提高持续集成的价值,所以我们会首选更详尽的测试。
单一代码源
为了实现每日集成,任何开发者都需要能够很容易地获取全部最新的源代码。以前,如果要做一次集成,我们就必须跑遍整个开发中心,询问每一个程序员有没有新的代码,然后把这些新代码拷贝过来,再找到合适的插入位置……没有什么比这更糟糕的了。
办法很简单。任何人都应该可以带一台干净的机器过来,连上局域网,然后用一条命令就得到所有的源文件,马上开始系统的创建。
最简单的解决方案就是:用一套配置管理(源代码控制)系统作为所有代码的来源。配置管理系统通常都设计有网络功能,并且带有让开发者轻松获取源代码的工具。而且,它们还提供版本管理工具,这样你可以很轻松地找到文件以前的版本。成本就更不成问题了,CVS就是一套出色的开放源代码的配置管理工具。
所有的源文件都应该保存在配置管理系统中。我说的这个“所有”常常比人们想到的还要多,它还包括创建脚本、属性文件、数据库调度DLL、安装脚本、以及在一台干净的机器上开始创建所需的其他一切东西。经常都能看到这样的情况:代码得到了控制,但是其他一些重要的文件却找不到了。
尽量确保所有的东西都保存在配置管理系统的同一棵代码源树中。有时候为了得到不同的组件,人们会使用配置管理系统中不同的项目。这带来的麻烦就是:人们不得不记住哪个组件的哪个版本使用了其他组件的哪些版本。在某些情况下,你必须将代码源分开,但是这种情况出现的几率比你想象的要小得多。你可以在从一棵代码源树创建多个组件,上面那些问题可以通过创建脚本来解决,而不必改变存储结构。
自动化创建脚本
如果你编写的是一个小程序,只有十几个文件,那么应用程序的创建可能只是一行命令的事:javac *.java。更大的项目就需要更多的创建工作:你可能把文件放在许多目录里面,需要确保得到的目标代码都在适当的位置;除了编译,可能还有连接的步骤;你可能还从别的文件中生成了代码,在编译之前需要先生成;测试也需要自动运行。
大规模的创建经常会耗费一些时间,如果只做了一点小小的改动,当然你不会希望重新做所有这些步骤。所以好的创建工具会自动分析需要改变的部分,常见的方法就是检查源文件和目标文件的修改日期,只有当源文件的修改日期迟于目标文件时,才会重新编译。于是,文件之间的依赖就需要一点技巧了:如果一个目标文件发生了变化,那么只有那些依赖它的目标文件才会重新编译。编译器可能会处理这类事情,也可能不会。
取决于自己的需要,你可以选择不同的创建类型:你创建的系统可以有测试代码,也可以没有,甚至还可以选择不同的测试集;一些组件可以单独创建。创建脚本应该让你可以根据不同的情况选择不同的创建目标。
你输入一行简单的命令之后,帮你挑起这副重担常常是脚本。你使用的可能是shell脚本,也可能是更复杂的脚本语言(例如Perl或Python)。但是很快你就会发现一个专门设计的创建环境是很有用的,例如Unix下的make工具。
在我们的Java开发中,我们很快就发现需要一个更复杂的解决方案。Matt用了相当多的时间开发了一个用于企业级Java开发的创建工具,叫做Jinx。但是,最近我们已经转而使用开放源代码的创建工具Ant(http://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编写这些测试,与开发使用同样的语言,所以编写测试与编写代码没有太大的区别。我们使用JUnit(http://www.junit.org/)来作为组织、编写测试的框架。JUnit是一个简单的框架,让我们可以快速编写测试、将测试组织为套件、并以交互或批处理的模式来运行测试套件。(JUnit是xUnit家族的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服务器上。