持续集成
Martin Fowler
ThoughtWorks 首席科学家
Matthew Foemmel
ThoughtWorks
可靠的软件构建是软件开发过程的一个重要部分。尽管它如此重要,我们还是经常吃惊地看到并没有那么做。在这里我们讨论一下Matt(Matthew Foemmel) 在 ThoughtWorks 一个重要项目中实施构建的过程,它正在全公司中得到更广泛的应用。它致力于一个每日自动化运行多次的构建,包括自动化的测试过程。这使得每个开发员减少每日集成遇到的问题。
ThoughtWorks 公司已经把自动化持续集成软件 CruiseControl 开放源码,并提供 CruiseControl, Ant, and getting C.I. 咨询。如果你感兴趣可继续阅读 Bill Caputo 和 Oren Miller 的文章 Continuous Integration with C++.
² 持续集成的好处
² 唯一代码源
² 自动化构建脚本
² 自测试代码
² 主要构建
² 源代码Check In
² 总结
软件开发充满了经常被讨论和实施的最佳实践。其中一个最基础的,最有价值的是完全的自动化构建过程,使得一个开发团队在一天中多次构建并测试软件。每日构建的想法已经讨论过多次了,McConnnell 建议它作为一个开发最佳实践。每日构建还是微软开发的特点之一。我们的想法和 XP 团体一致,每日构建是最小需求。每日多次完全的自动构建是可以做到并且是值得做的。
我们使用的词语"持续集成"来自XP(极限编程) 的最佳实践。我们注意到,这个实践已经在存在很久了,而且有很多人在使用,但是他们并没有意识到是XP的最佳实践。我们把XP当成软件开发过程的试金石并影响了很多术语和实践。然而你可以应用持续集成但并不应用XP的其它部分。确实,我们认为这个过程是所有软件开发过程的最本质的部分。
自动化每日构建包括下面几个部分。
保持唯一一个源代码库。任何人可以从这里获取当前的代码或者以前的版本的代码。
构建过程自动化。任何人可以通过简单的命令完成构建过程,可从源代码构建系统。
自动化测试。可以通过简单命令在任何时间运行测试用例组。
确定任何人可以获取当前有信心可以最好的运行可运行版本。
所有这些都遵循这一些相应的规则。我们发现把他们带入项目要花很大力气。我们也发现,这个过程一旦建立起来,保持下去并不花费太多。
持续集成的好处
关于持续集成,最难表达的事情是关于对整个开发模式的基础变革。如果你从没有在一个环境中实践它,这一点很不容易发现。事实上大多数独自工作的人能看到这一点,因为他们仅仅需要集成自己。对许多开发团队中的人们来说,团队开发是和一些这个领域的问题一起出现的。持续集成能用相当数量的规则减少这样的问题。
持续集成最基本的好处是它能省去花费人们大量时间的“除虫(bug)”会议。缺陷,当团队中的一个人的工作跨入其它人的工作而自己还没有意识到的时候产生的。这种bug 非常难发现,因为问题不是出在一个人身上,而是产生在两部分人的交互上。这些问题随着时间不断恶化。通常,集成方面的缺陷在被发现并列表出来之前几周或者几个月之前就已经产生了,还花费了很大力气发现这些缺陷。
采用了持续集成,大多数的这种缺陷在产生的同一天就会被发现,并可以进一步找出出错位置。这大大减少了查找缺陷的范围。如果找不到缺陷,也可以避免这些问题代码进入产品,避免加入特性到产品中的同时也加入了缺陷。(当然你可能更想要这个特性,胜过对缺陷的憎恨,但是至少是一个选择。)
目前还不能保证能找到所有集成带来的缺陷。这项技术依赖于测试,而且正如我们大家所知,测试并不能证明错误的存在。关键是持续集成可以发现足够多的缺陷,与花费相比是值得做的。
继续集成的纯粹的结果是通过减少集成带来的缺陷而提高生产力。尽管我们还不知道是否有人已经对此进行科学研究,但确实是相当有效的实践方法。
集成越多次越好
持续集成有一个基本的量化-直觉效果,就是经常性的集成比很少的集成要好。对于那些这样做的人来说这是很明显的,但是对那些还没有这样做的人来说这是和他们的直接经验相矛盾的。
如果你只是偶尔进行集成,比如少于每日集成,那么集成是一个痛苦的经历,那会消耗大量的时间和精力。确实,如果你最后想做的是更经常的去做真是够惨的。我们常听到的建议是“在这样大型的项目里,每日构建是不可能做到的。”
然而有项目做到了。在一个大约200000行代码的、50个人的团队的项目,我们每天执行24次构建。微软也曾成功地在千万行代码的项目实行每日构建。
能做到的原因可能是集成所需的花费和集成间隔时间成指数比例。尽管我们还没有量化这个比例,但估计每周构建一次并不是花费每日构建的5倍时间,可能是25倍。所以如果你觉得集成是一个痛苦过程,不能把它当成你不能更频繁集成的迹象。如果做得好,更频繁的集成不会带来麻烦,而是花费更少时间的集成。
一个关键点是自动化。多数集成能也应该是自动执行的。获取源代码,编译,连接,重要的测试都可以自动执行。你只是简单的获得一个构建工作的提示:成功还是失败。如果失败,你应该可以容易的撤销最后的修改。得到一个可运行版本,不需要动什么脑筋。
有了自动化构建过程,你可以多频繁构建都行。唯一的限制是构建本身花费的时间。
什么是成功的构建?
成功的构建是一个重要的问题。看起来很简单,但有时却变得很糟。有一次Martin Fowler回顾一个项目,他问项目是否做每日构建,回答是肯定的。幸运的是,Ron Jeffries 也在那里,他进一步的询问。他问:“你们怎样处理构建的错误?”回答是:“我们发送e-mail个给相关人员。”事实上这个项目已经一个月没有成功构建了。这不是每日构建,而只是尝试每日构建。
对于成功的构建,我们是很有信心的。
² 所有最新源代码从配置管理系统 checkout
² 所有文件通过编译
² 编译后的目标文件(在这里是Java classes) 发布并可运行
² 启动系统运行,针对系统的测试用例集合 (在我们这里大约150 测试用例)运行
² 如果上述过程执行没有错误和人为干涉,而且测试都通过,那么我们说是一个成功的构建
大多数人认为一个构建是一个编译和一个连接。但我们认为还应包括系统运行并进行一些简单的测试。(McConnnell 使用术语“冒烟测试”:打开让它运行看它是否冒烟)。运行更完备的测试集合能显著地提高持续集成的价值,所以我们更喜欢那样做。
为了使集成更容易,任何开发人员都需要能很容易地得到所有当前源代码。如果在构建之前不得不到处找开发员要最新的代码并且要手工拷贝过来,并指定拷贝的位置,那没有什么比这更糟糕了。
标准很简单。每个人应该可以找到一台干净的机器,连接上网络,用一个简单的命令得到正在开发的源代码。
显而易见,解决方案是使用配置管理系统(源代码控制) 作为所有代码的源。配置管理系统通常设计地通过网络使用,有使用简便的工具维护代码。此外还包括版本管理,可以很容易得到文件的以前的版本。不需考虑成本, CVS 就是一个开源的配置管理工具。
所有源代码文件被保存在配置管理系统中。这个“所有”通常比人们想到的要多。还包括构建脚本、属性文件、数据库模式DDL、安装脚本和任何其它构建所需要的。常见问题是有了代码控制但是忽略了其它必需的文件。
尽量保证所有都在一个配置管理系统的唯一代码源树下。有时人们为不同组件在配置管理系统使用不同的工程。这样带来的麻烦是人们会忘记哪个组件的哪个版本和其它组件的哪个版本可以一起运行。在一些条件下你不得不分散源代码,但这样的情况很少见。你可以在一个单一代码源构建多个组件,这样上面的问题应通过构建脚本解决,而不是存储结构。
如果你是开发一个小应用,有十几个程序文件,构建仅仅就是执行简单的编译命令:javac *.java。更大的项目需要更多的工作,你可能有许多文件在许多目录中。你还要确认结果代码在适当的位置。除了编译还有连接的步骤,还可能需要在编译之前从别的文件生成代码,测试也需要自动运行。
大型的构建经常是耗时的,你可能只是做了很小的改动,不需要做那么多步骤。所以需要一个好的构建分析工具分析需要改变的作为构建的过程一部分。通常做法是检测源代码文件的和目标代码的时间,只是编译那些时间更近的源代码。处理程序文件之间的依赖就需要技巧了,如果一个目标文件变化了,依赖它的文件也需要重新编译。编译器可能能处理这种情况,也可能不处理。
取决于你的需要,你可能需要各种不同的构建。你可以构建系统同时进行测试,或者不测试,或者执行不同的测试集。一些组件可以单独构建。构建脚本应该可以让你为不同情景设置不同创建目标。
一旦你输入一个简单的命令,构建脚本开始运行。可以使用操作系统外壳脚本,或者使用一些优秀的脚本语言如Perl、Python。但很快会意识到应使用专门为此设计的东西如Unix 平台下的make工具。
在我们的Java开发过程中,我们很快发现我们需要更高级的解决方案。Matt花费了一点时间开发了一个构建工具叫做Jinx,是为企业级的Java工程设计的。近来我们开始转移到使用开源的构建工具 Ant 。Ant的设计和Jinx很类似,它允许我们编译Java文件并打包成Jar文件。它也允许我们自己扩展Ant在构建时运行其它任务。
我们中的许多人使用IDE(集成开发环境),而且大多数IDS有一些构建管理过程。但是这些功能只是适用于这个IDE工具,功能很脆弱,依赖于IDE运行。IDE的用户建立自己的工程文件并用于自己的开发工作。然而,我们依赖Ant执行构建工作,主构建在一台服务器上运行。
仅仅编译程序是不够的。尽管编译器能发现许多错误,还是会有许多错误出现在成功的编译后。为了跟踪这些错误我们重点强调自测试规则,一个XP推崇的实践方法。
XP 把测试分为两种。一个是单元测试,一个是确认测试(功能测试)。单元测试是开发人员写的程序通常用来测试单个的类或者一小组类。确认测试通常是客户或者外部的测试组(在开发人员的帮助下)开发的。这两种测试我们都使用了,并尽可能的进行自动化测试。
作为构建的一部分,我们运行一个测试用例组,叫做“BVT”(Build Verification Tests)。为获得一个成功的构建,BVT中的所有测试必须通过。所有XP风格的测试都包含在“BVT”。本文是关于构建过程的,所以我们会在这里提到“BVT”。但要记住还有一个测试线存在,即功能测试,不要仅仅基于BVT评估测试和QA的成果。确实我们的QA组从来没有看到代码,因为他们只是基于可运行的构建进行测试工作。
一个基本原则是开发者在写代码时也要写测试代码。他们完成任务时不只是 check in 产品代码,也需要 check in 测试代码。这紧紧遵循了XP的测试优先的编程风格。如果你想增加一个新特性到系统中,你首先要写这个特性的测试代码,并使得测试可以在特性实现之后运行。
我们使用Java语言开发,测试代码也是Java语言开发的。我们使用 JUnit 作为组织和编写测试的框架。JUnit 是一个简单易用的测试框架,用于快速开发测试,组织测试用例,可以批处理方式运行测试用例集合。(JUnit 是 xUnit 家族成员,xUnit还有其它语言的版本。)
典型情况是开发人员在每次编译时都运行一部分单元测试,这样确实能提高开发效率。测试对发现代码中的逻辑错误很有帮助,就不需要调试。因为此时只需关注变化的代码,这样容易发现缺陷。
并不是每个人都严格按照 XP 测试有限的风格工作,同时他们也得不到测试带来的益处。测试使得在加快本人开发任务同时,BVT的运行也更容易抓住错误。BVT每天都会运行好多次,任何BVT检测到的错误都很容易发现。这和前面说到测试能更容易发现错误的原因是一样的:我们此时只需关注变化的代码,在变化的代码中调试效率更高。
当然,你不能依赖测试发现所有错误。就像我们常说:测试并不能证明不存在缺陷。尽善尽美不是你进行良好的BVT的唯一回报。不完美的经常运行的测试比从来没有的完美测试好的多。
一个相关问题是开发人员在自己的代码中写测试代码。我们常说人们不应该自己测试代码,因为人们很容易忽视自己的错误。这是事实,但是自测试过程需要把测试快速转变到基础代码中。这个快速转变的价值大于测试者的独立。所以 BVT 还是依赖开发者写的测试,但还是要有独立编写的确认测试。
自测试的另外一个要点是通过反馈提高测试质量,这是XP的一个关键价值点。这里的反馈以逃过BVT的缺陷的形式存在。这里的规则是除非在 BVT 的单元测试执行失败你不能改正任何缺陷。每次改正一个缺陷,你应该在加入一个新的测试以确保它不再逃过BVT。更进一步你还应该考虑加入其它新的测试使得 BVT 更健壮。
自动构建对单个开发者意义重大,但要让它真正发光是在整个团队的主构建上。我们发现主构建过程能把整个团队组织在一起,能更早更容易发现集成的问题。
第一步是选择一台机器做主构建。我们选择了Trebuchet(投石机,我们经常玩帝国时代),一台4CPU的服务器专门执行构建过程。(由于完整的创建需要相当长的时间,所以这种马力是必须的。)
构建进程是一个Java类,它一直运行。如果没有构建在执行,它就进入循环状态,并每个几分钟检查一次代码库。如果有人 check in 代码,它就马上开始构建。
构建的第一阶段是做一个代码库完全的 check out。Starteam(译者按:Borland 公司出品的配置管理系统)提供了相当好的 Java API,可以直接存取代码库。构建后台进程判断近5分钟来是否没有人 check in 代码,如果是则执行 check out(这样防止有人在 check in 时 check out)
后台进程 check out 代码到Trebuchet上,然后调用 Ant 脚本执行构建。我们对所有源代码执行完全构建。Ant 脚本编译 Java 类并打包为十几个 jar 文件部署到 EJB 服务器。
Ant 编译和部署完成后,构建后台进程启动 EJB 服务器并执行 BVT 测试集合。如果测试通过则我们就完成了一个成功的构建。然后构建后台进程在 Startteam 代码库为源代码标记一个构建号。然后再检测是否有新的代码 check in,如果有则开始新一轮构建。
最后,构建后台进程发送一个 e-mail 给所有新 check in 代码的开发人员,在e-mail 中概述了构建的状态信息。check in 代码然后离开直到收到e-mail 被认为是一个讨人嫌的行为。
构建后台进程通过 XML 格式的日志记录每个步骤。通过在 Trebuchet 上运行的一个 servlet 能让所有人通过日志看到构建的状态。(见图1)
屏幕上会显示出构建是否正在运行、开始运行的时间。在左边有所有构建的历史记录,成功的、失败的都记录在案。点击其中的某一条记录,就会显示出这次创建的详细信息:编译是否通过、测试的结果、发生了哪些变化等等。
我们发现很多开发者都经常看看这个页面,因为它让他们看到项目发展的方向,看到随着人们不断 check in 代码而发生的变化。有时我们也会在这个页面上放一些其他的项目新闻,但是需要把握好尺度。
很重要的一点是:任何开发人员也可以在自己的机器上执行模仿主构建。这样当构建错误发生,可以在自己的机器上调试发现错误。另外,开发人员可以在 check in 之前在本机执行构建,以减少主构建的失败可能。
还有一个问题是主构建应该是 clean 构建,即完全从源代码开始,还是应该增量的构建。增量的构建比较快,但是也有漏过某些问题的风险,因为有些代码没有重新编译,还有不能重建构建的风险。我们的构建相当快(大约200KLOC/15分钟),所以我们乐于每次都执行 clean 构建。但是一些团队喜欢在多数时候执行增量构建,并执行有规律的clean构建(至少每天1次),有时出现奇怪的问题时也执行clean构建。
使用自动构建意味着开发人员按照一定的节奏开发软件。这个节奏最重要的是有规律的集成。我们遇到过执行每日构建的团队,但是开发员并不经常 check in 代码。如果开发员几周才 check in 代码,那么每日构建就没什么用了。我们信奉的原则是开发员每天都要 check in 代码。
在开始一个新任务前,开发员应该和配置管理系统同步。本地的代码拷贝必须是最新的。在过期的代码上继续开发只会带来麻烦和混乱。
然后,开发者要随时保持文件的更新。开发者可以在一段任务完成之后将代码集成到整个系统中,也可以在任务的中途集成,但是在集成的时候必须保证所有的测试都能通过。
集成的第一步是开发员的工作版本和配置库的同步。任何配置库已经更改的文件更新到工作版本,配置管理系统会对冲突发出警告。然后开发人员在同步后的工作版本上构建并运行 BVT。
现在开发人员可以提交新文件到代码库了。然后开发人员需要等待进行主构建。如果构建成功,那么 check in 就是成功的。 否则开发人员要解决问题。如果很容易改正了可以提交更新。如果很复杂,开发人员需要回退这次改动,重新同步本地的工作版本,继续修改。然后再次提交。
有时check in 过程要求是逐个进行的。同一时刻只有一个开发人员持有构建令牌,执行构建。这样做就使得在两次构建之间只有一个开发人员可以更新代码库。我们发现没有构建令牌很少会带来麻烦,所以我们没有使用。经常会有多个开发人员同时提交代码到主构建,但很少导致构建失败,即使有也很容易解决。
我们让开发人员自己判断 check in 代码的小心程度。这反映出开发者对集成错误的评估。如果他觉得可能出现集成错误,他会在本机先做个构建。如果她觉得不会出现集成错误,那么她可以直接check in 代码。如果有错误,在主创建运行时他就会发现,然后他就不得不回退自己的修改,找到出错的地方。如果错误很容易发现和解决,那么这种错误也是可以容忍的。
培养一个严格遵守的制度和自动构建过程对项目控制是很关键的。许多软件开发先贤都提到过,但我们发现仍旧很少有做到的。
关键是让所有事情自动化,并且经常执行构建,快速发现集成的错误。每个人随时准备修改需要他们修改的东西,因为他们知道:如果他们做的修改导致集成错误,那也是容易发现和改正的。一旦获得了这些利益,你会发现自己再也无法放下它们。