3.1 引言

很 多软件项目都有一个非常奇怪而又常见的特征,即在开发过程里,应用程序在相当长的一段时间内无法运行。事实上,由大规模团队开发的软件中,绝大部分在开发 过程中基本上处于不可用状态。其原因很简单,没有人有兴趣在开发完成之前运行整个应用。虽然开发人员提交代码后可能会运行自动化的单元测试,但没人会在试 运行环境中去启动并使用它。

在 那些分支生命周期很长或者直到最后才做验收测试的项目里尤其如此。许多像这样的项目总是在开发结束后留出很长一段时间作为集成阶段。在该阶段里,开发团队 会合并分支,让软件能够运行起来,以便进行验收测试。甚至更糟糕,有些项目可能到了集成阶段才发现软件并不能满足需求。这样的集成活动可能会持续很长时 间,而最糟糕的则莫过于没人知道到底要花多长时间。

可 是,我们也看到,某些项目即便最新提交的代码破坏了已有功能,最多也只要几分钟就可修好。其不同之处在于后者使用了持续集成。持续集成要求每当有人提交代 码时,就对整个应用进行构建,并对其执行全面的自动化测试集合。而且至关重要的是,假如构建或测试过程失败,开发团队就要停下手中的工作,立即修复它。持 续集成的目标是让正在开发的软件一直处于可工作状态。

持续集成最早出现在Kent Beck写的《解析极限编程》一书中,该书于1999年首次出版。和其他极限编程实践一样,持续集成背后的思想是:既然经常对代码库进行集成对我们有好处,为什么不随时做集成呢?就集成而言,“随时”意思是指每当有人提交代码到版本控制库时。我的同事Mike Roberts说:“持续的频繁程度远超出你的想象。”[aEu8Nu]

持 续集成是一种根本的颠覆。如果没有持续集成,你开发的软件将一直处于无法运行状态,直至(通常是测试或集成阶段)有人来验证它能否工作。有了持续集成以 后,软件在每次修改之后都会被证明是可以工作的(假如有足够全面的自动化测试集合的话)。即便它被破坏了,你也很快就能知道,并可以立即修复。高效使用持 续集成的那些团队能够比那些没有使用它的团队更快地交付软件,且缺陷更少。在交付过程中,缺陷被发现得越早,修复它的成本就越低,因此也就大大节省了成本 和时间。因此我们认为,对于专业的软件交付团队来说,持续集成与版本控制同等重要。

本章剩余内容将讲述如何实现持续集成。我们会解释如何解决复杂项目中的常见问题,列出有效的持续集成实践及其对设计与开发过程的影响。我们还会讨论一些更高级的话题,包括如何在分布式团队中实施持续集成。

关于持续集成,本书的姊妹篇,即Paul Duvall写的《持续集成》(Addison-Wesley, 2006)介绍得更为详细。如果想知道比本章所述更多的内容,请参阅这本书。

本章主要面向开发人员,但其中的某些内容对于想知道更多持续集成相关实践的项目经理也是非常有帮助的。

3.2 实现持续集成

“持续集成”这一实践并非信手拈来,它需要有一定的先决条件。我们先介绍这些先决条件,然后再看一看有哪些工具可以利用。也许最重要的一点是,“持续集成”依赖于那些能够遵守一些重要实践的团队,所以我们也会花上一点时间来讨论一下。

3.2.1 准备工作

在开始做持续集成之前,你需要做三件事情。

  1. 版本控制

与 项目相关的所有内容都必须提交到一个版本控制库中,包括产品代码、测试代码、数据库脚本、构建与部署脚本,以及所有用于创建、安装、运行和测试该应用程序 的东西。听上去这些都是理所当然的事情,可奇怪的是,的确有些项目没有使用版本控制。有些人认为,他们的项目不大,用不着使用版本控制。可在我们看来,现 在没有哪个项目小到可以不用它。即便在自己的电脑中为自己写一些代码的话,我们也会使用版本控制。现在有好几个简单易用、功能强大且轻量级的免费版本控制 工具。

关于版本控制工具的选择和使用,我们分别在2.2节和第14章中详细讲述。

  1. 自动化构建

你要能在命令行中启动构建过程。无论是通过命令行程序启动IDE来构建应用程序,然后再运行测试,还是使用多个复杂的构建脚本通过互相调用的方式来完成都行,但无论采用哪种机制,必须满足如下条件:人和计算机都能通过命令行自动执行应用的构建、测试以及部署过程。

现 在,集成开发环境和持续集成工具的功能都非常强大。通常不需要切换到命令行,你就可以用集成开发环境完成应用程序的构建,并执行测试。然而,我们仍认为, 你仍然需要有能力通过命令行执行,而不需要使用集成开发环境的构建脚本。对于这一点,可能存在一些争议,但我们的理由如下。

要能在持续集成环境中以自动化的方式来执行整个构建过程,以便出现问题时能够审计。

应将构建脚本与代码库同等对待。应该对它进行测试,并不断地重构,以使它保持整洁且容易理解,而集成开发环境自动生成的构建过程基本上无法做到这一点。项目越复杂,这项工作就越重要。

使理解、维护和调试构建过程更容易,并有利于和运维人员更好地协作。

  1. 团队共识

持 续集成不是一种工具,而是一种实践。它需要开发团队能够给予一定的投入并遵守一些准则,需要每个人都能以小步增量的方式频繁地将修改后的代码提交到主干 上,并一致认同“修复破坏应用程序的任意修改是最高优先级的任务”。如果大家不能接受这样的准则,则根本无法如预期般通过持续集成提高质量。

3.2.2 一个基本的持续集成系统

为了做持续集成,你不一定就需要一个持续集成软件,正如我们所说,它是实践,并不是工具。James Shore在“Continuous Integration on a Dollar a Day[①]”[bAJpjp]一文中描述了一个非常简单的方法,只需要一台闲置的开发机,一个橡胶做的玩具鸡和一个桌上震铃。这篇文章值得一读,从中可以看出,只要有版本控制工具就可以做持续集成了。

事实上,现在的持续集成工具其安装和运行都极其简单。有几个开源工具可供选择,比如Hudson和受人尊敬的CruiseControl家族(CruiseControl、CruiseControl.NET和CruiseControl.rb)。其中,Hudson和CruiseControl.rb的启动和运行尤其简单。CruiseControl.rb是很轻量级的,而且掌握一些Ruby知识的人很容易对它进行扩展。Hudson的插件很多,这使它可以与构建和部署领域中的很多工具集成。

在此书编写之际,还有两种商业化持续集成服务器为小团队提供了免费版本,它们是ThoughtWorks Studios开发的Go以及JetBrains的TeamCity。其他流行的商业化持续集成服务器还包括Atlassian的Bamboo和Zutubi的Pulse。高端的发布管理以及构建加速系统还有UrbanCode的AntHillPro、ElectricCloud的 ElectricCommander,以及IBM的BuildForge,它们都可以用于简单的持续集成。还有很多其他产品,完整列表可参见CI feature matrix[②]。[bHOgH4]

假 如能够满足前面所述的先决条件,那么当你选择并安装好持续集成工具之后,只要再花几分钟的时间配置一下就可以工作了。这些配置包括让它知道到哪里寻找源代 码控制库,必要时运行哪个脚本进行编译,并执行自动化提交测试,以及一旦最新的提交破坏了应用程序,通过哪种方式通知你。

第 一次在持续集成工具上执行构建时,你很可能发现在运行持续集成工具的机器上缺少一些必需的软件和设置。这是一个独一无二的学习机会,请将接下来你所做的工 作全部记录下来,并放在自己项目的知识共享库中。你应该花上一些时间将应用程序所依赖的所有软件和配置项提交到版本控制系统中,并将重建全新环境的整个活 动变成一个自动化的过程。

接下来要让所有人开始使用这个持续集成服务器。下面是一个简单的过程。

一旦准备好要提交最新修改代码时,请遵循如下步骤。

(1) 查看一下是否有构建正在运行。如果有的话,你要等它运行完。如果它失败了,你要与团队中的其他人一起将其修复,然后再提交自己的代码。

(2) 一旦构建完成且测试全部通过,就从版本控制库中将该版本的代码更新到自己的开发环境上。

(3) 在自己的开发机上执行构建脚本,运行测试,以确保在你机器上的所有代码都工作正常。当然你也可以利用持续集成工具中的个人构建功能来完成这一步骤。

(4) 如果本地构建成功,就将你的代码提交到版本控制库中。

(5) 然后等待包含你的这次提交的构建结果。

(6) 如果这次构建失败了,就停下手中做的事,在自己的开发机上立即修复这个问题,然后再转到步骤(3)。

(7) 如果这次构建成功,你可以小小地庆祝一下,并开始下一项任务。

如果团队中的每个人在每次提交代码时都能够遵循这些简单的步骤,你就可以很有把握地说:“只要是在与持续集成一模一样的环境上,我的软件就可以工作。”

3.3 持续集成的前提条件

持续集成不会独立地帮你修复构建过程。事实上,如果你在项目中期才做这件事的话,可能会非常痛苦。为了使持续集成能够更有效,开始之前,你应该先做好下面这些事情。

3.3.1 频繁提交

对于持续集成来说,我们最重要的工作就是频繁提交代码到版本控制库。每天至少应该提交几次代码。

定期地将代码提交到代码主干上会给我们带来很多其他好处。比如,它使每次的修改都比较小,所以很少会使 构建失败。当你做了错事或者走错了路线时,可以轻松地回滚到某个已知的正确版本上。它使你的重构更有规则,使每次重构都是小步修改,从而保证可预期的行 为。它有助于保证那些涉及多个文件的修改尽量不会影响其他人的工作。它让开发人员更敢于创新,勇于尝试新的想法,而且一旦行不通,可以轻松地回滚到最近提 交的一个版本上。它还会让你不时地停下来休息一下,伸展一下身体,有助于防止腕关节疼痛或肢体重复性劳损(RSI)。如果发生了严重的问题(比如误删了文件等),你也不会丢掉太多的工作成果。

前面我们特意提到过“要提交到主干”。很多项目使用版本控制中的分支技术来进行大型团队的管理。然而, 当使用分支时,其实不可能真正地做到持续集成。因为如果你在分支上工作,那么你的代码就没有和其他开发人员的代码进行即时集成。那些使用长生命周期分支的 团队恰恰面临着我们在本章开始时描述的集成问题。除一些很有限的情况外,我们不推荐使用分支。我们会在第14章更详细地讨论这个问题。

3.3.2 创建全面的自动化测试套件

如果没有一系列全面的自动化测试,那么构建成功只意味着应用程序能够编译并组装在一起。虽然对于某些团 队来说,这已经是非常大的一个进步了,但是,假如能够有一定程度的自动化测试,会让你更有信心说:“我们的应用程序是可以工作的。”自动化测试有很多种, 我们会在下一章详细讨论。其中有三类测试我们会在持续集成构建中使用,它们分别是单元测试、组件测试和验收测试。

单元测试用于单独测试应用程序中某些小单元的行为(比如一个方法、一个函数,或一小组方法或函数之间的 交互)。它们通常不需要启动整个应用程序就可以执行,而且也不需要连接数据库(如果应用程序需要数据库的话)、文件系统或网络。它们也不需要将应用程序部 署到类生产环境中运行。单元测试应该运行得非常快,即使对于一个大型应用来说,整个单元测试套件也应该在十分钟之内完成。

组件测试用于测试应用程序中几个组件的行为。与单元测试一样,它通常不必启动整个应用程序,但有可能需要连接数据库、访问文件系统或其他外部系统或接口(这些可以使用“桩”,即stub技术)。组件测试的运行时间通常较长。

验收测试的目的是验证应用程序是否满足业务需求所定义的验收条件,包括应用程序提供的功能,以及其他特 定需求,比如容量、有效性、安全性等。验收测试最好采用将整个应用程序运行于类生产环境的运作方式。当然,验收测试的运行时间也较长。一个验收测试套件连 续运行一整天是很平常的事儿。

通过组合使用这三类测试,你就能确信引入的修改不会破坏任何现有功能。

3.3.3 保持较短的构建和测试过程

如果代码构建和单元测试的执行需要花很长时间的话,你会遇到一些麻烦,如下所示。

大家在提交代码之前不愿意在本地环境进行全量构建和运行测试,导致构建失败的几率越来越大。

持续集成过程需要花太长时间,从而导致再次运行构建时,该构建会包含很多次提交,所以很难确定到底是哪次提交破坏了本次构建。

大家提交的频率会变少,因为每运行一次构建和测试,都要坐在那儿等上一阵子。

理想情况下,提交前的预编译和测试过程,以及持续集成服务器上的编译和测试过程应该都能在几分钟内结束。我们认为,十分钟是一个极限了,最好是在五分钟以内,九十秒内完成是最理想的。十分钟对于那些惯于操作小项目的人来说,应该算是比较长的时间了,但对于那些经历过需要花数小时的编译的老前辈来说,却是非常短的时间。这段时间长度应该恰好能泡杯茶,快速聊几句,看一眼邮件,或伸展一下身体。

接下来的这个要求看上去恰好和上一个(即需要有全面的自动化测试集)相矛盾。但是,有很多技术可以帮助你减少构建时间。首先要考虑的事情是让测试执行得更快。XUnit类型的工具,比如JUnit和NUnit,可以提供每个测试运行时长的报告。找出那些运行较慢的测试,看看是否可以把它们优化一下,或者在确保同样覆盖率和信心的前提下缩短测试时间。这件事情应该经常做。

然而,有时候需要将测试分成几个阶段,如第5章所述。那么如何划分阶段呢?首先将其分成两个阶段。第一个阶段用于编译软件,运行所有类级别的单元测试,并创建用于部署的二进制文件。这个阶段叫做“提交阶段”。在第7章我们会非常详细地讨论这个阶段。

第二个阶段应该利用第一个阶段所生成的二进制文件进行验收测试、集成测试。假如你有性能测试的话,也要一并运行。利用现代持续集成工具,很容易创建这种分阶段的构建流程,它们能够同时运行多个任务,并将运行结果收集在一起,以便很容易看到运行状态和结果。

提交阶段的这套测试应该在提交之前运行,而且在每次提交之后,在持续集成服务器上也要再运行一次。一旦 提交测试套件通过了,就要马上运行验收测试的第二个阶段,但这个阶段可能会花更多时间。如果该阶段的用时超过半小时,就要考虑使用高性能的多进程机器或者 建立构建网格来并行执行这些测试。现代的持续集成服务器都能让这件事变得很简单。另外,有时候把一个简单的冒烟测试套件加入到提交阶段,也是非常有用的。 这个冒烟测试套件应该执行一些简单的验收和集成测试,用于确保最常见的功能没有被破坏。假如这些基本功能被破坏了,就能得到很快的反馈。

将验收测试按功能块进行分组通常是可取的。这样,当仅修改了系统中的个别功能块时,就可以单独运行影响系统这部分功能的验证测试。很多单元测试框架都提供这样的分组功能。

有时候,你会遇到这种情况,项目由几个模块组成,而每个模块的功能相对独立。此时需要认真考虑如何在版本控制库和持续集成服务器上合理地组织这些模块。我们将在第13章详细描述这部分内容。

3.3.4 管理开发工作区

对 于保证开发人员的开发效率与明晰思路来说,开发环境的管理是特别重要的。当开发人员刚开始新任务时,应该总是从一个已知正确的状态开始。他们应该能够运行 构建、执行自动化测试,以及在其可控的环境上部署其开发的应用程序,通常是在他们自己的开发机上。只有在特殊的情况下,才应使用共享环境开发。在本地开发 环境上运行应用程序时,应确保所使用的自动化过程与持续集成环境中的一致,与测试环境中也是一样的,且生产环境中也是一样的。

达 到这一目标的第一个先决条件就是细心的配置管理,不仅仅是管理代码,还包括测试数据、数据库脚本、构建脚本和部署脚本,这些全部都要放在版本控制库中,且 当编码开始时,应该以它们“最新的正确版本”作为起点。“最新的正确版本”是指那个在持续集成服务器上最近一次通过所有自动化测试的那个版本。

其次是对第三方依赖的配置管理,即那些开发中所用的库文件和组件。应确保库文件或组件的版本都是正确的,即它们的版本与你正在开发的源代码的版本是相互匹配的。有些开源工具可以帮助管理第三方依赖,最为常见的有Maven和Ivy。然而,使用这些工具时,你需要格外小心地确保正确配置这些工具,这样才能保证不必每次都将某些第三方依赖的最新版本重新下载到本地仓库中。

对于大部分项目来说,其所依赖的第三方库文件的版本不会经常发生改变,所以最简单的方法就是将这些库文件随你的代码一起提交到版本控制库中。关于这一点,更多的内容请参见第13章。

最 后就是确保自动化测试(包括冒烟测试)都能够在开发机上运行。对于一个大型系统,我们可能需要在开发机上配置中间件,运行内存数据库或单用户数据库。这的 确要花一定的功夫,但能够让开发人员于每次提交前在自己的开发机上将应用程序运行起来,并在其上跑一遍冒烟测试,这可以大大改善应用程序的质量。事实上, 一个好的应用程序架构的标志就是不需要费太大力气就可以让应用运行在开发机上。

3.4 使用持续集成软件

当今市场上有很多产品可以提供针对自动化构建和测试过程的基础设施。持续集成工具最基本的功能就是轮询版本控制系统,查看是否有新的版本提交,如果有的话,则签出最新版本的软件,运行构建脚本来编译应用程序,再运行测试,最后将运行结果告知你。

3.4.1 基本操作

本质上,持续集成软件包括两个部分。第一部分是一个一直运行的进程,它每隔一定的时间就执行一个简单的工作流程。第二部分就是提供展现这个流程运行结果的视图,通知你构建和测试成功与否,让你可以找到测试报告,拿到生成的安装文件等。

通常,持续集成工作流以规定的时间间隔对版本控制系统进行轮询。一旦发现版本库有任何变化,它就会将项目的一个副本签出到服务器或构建代理机器的某个目录中,然后运行你指定的命令。典型情况下,这些命令会构建你的应用程序,并运行相关的自动化测试。

大多数持续集成服务器包括一个Web服务器,用于展示一个列表,列出所有已运行过的构建(图3-1),允许查看结果报告,即每次构建的结果是成功,还是失败。这一系列的构建应该终止于生产环境,并保存所有成果,比如二进制文件或安装包,以便测试人员和客户可以很方便地下载最新的可工作版本。大多数持续集成服务器都可以通过Web界面或简单的脚本进行配置。

enter image description here

3.4.2 铃声和口哨

你可以利用持续集成工具的工作流功能做一些基本功能之外的事情。比如,可以将最近一次的构建状态发送到一个外部设备上。曾经有人使用红绿熔岩灯显示最后构建的状态,或使用持续集成工具将状态发送给Nabaztag无线电子兔。我们认识的一位开发人员了解一些电子知识,他做了一个带有闪光灯和警报器的灯塔,其动作可指示一个复杂项目上各种构建的进展情 况。还有一种方式是使用文字到语音的切换功能读出令构建失败的提交人的名字。一些持续集成服务器可以显示构建的状态,以及提交者的照片,而且可以使用一块 大屏幕把它们显示出来。

在项目中使用这类小装置的原因很简单,这种方法非常有效,可以让每个人一眼就知道构建的状态。可视化是使用持续集成服务器软件最重要的收益之一。大多数持续集成服务器软件都会提供某种小装置,可以安装在开发机上,在电脑桌面的一角上显示构建状态。对于分布式团队(至少是不在同一房间中工作的团队)来说,这种小工具特别有用。

这 种可视化的唯一缺点就是,如果开发团队和客户在一起工作的话(对于大多数敏捷项目来说,的确是这样的),构建失败(流程中很自然的一部分)可能被认为是应 用程序质量存在问题的信号。事实也正是如此,每次构建失败都表明发现了一个问题,但如果没有发现的话,它就会被带到生产环境中。然而,有时候很难向客户解 释“为什么构建总是失败”。我们曾遇到过好几次这种状况,其中有一次构建失败持续了很长时间,期间我们与客户进行了一些艰难的对话,但唯一能做的事情就是 让它高度可视化,并努力工作,向客户解释这样做的好处。当然,最佳解决方案是努力工作,让构建一直成功。

你还可以在构建过程中对源代码进行一些分析工作,包括分析测试覆盖率、重复代码、是否符合编码标准、圈复杂度,以及其他一些健康指标,并将结果显示在每个构建的总结报告中。你也可以运行一些程序来生成与代码相对应的对象模型图或数据库结构图。所有这些都是可视化的一部分。

今天,先进的持续集成服务器还能够将工作分发到构建网格中,并管理这些构建以及多个组件的依赖集合,将结果发送到你的项目管理跟踪系统中,并有其他一些很有用的功能。

持续集成的前身

在使用持续集成之前,很多开发团队都使用每日构建(nightly build)。当时,微软使用这个实践已经很多年了。谁破坏了构建,就要负责监视后续的构建过程,直至发现下一个破坏了构建的人。

现在很多项目仍旧在使用每日构建的做法,在每天晚上,所有人都回家以后,通过批处理过程对代码库进行编 译集成,这么做的确有一定的益处。但是,假如第二天早上,团队发现代码根本无法编译成功,这显然就没有太大的帮助了。因为第二天团队还是会向代码库中再提 交新的修改,可直到第二天晚上才能再次验证这个系统是否能够集成起来。久而久之,构建就天天都是红色的[③]了,而这种失败状态很可能会延续到最后的集成时间点才会被修复。另外,当团队不在同一地点办公,而

且是人员分散在不同的时区里,同时还使用同一个代码库时,这种做法基本上就没有什么用了。

接下来具有革命性的一步是增加自动化测试。我们首次尝试自动化测试是在很多年以前。毫无疑问,那时候都 是最基本的冒烟测试,简单地断言应用程序可以运行汇编。在当时,这是构建过程中我们引以为豪的非常大的一个进步。可是现在,即使是最基本的自动化构建过程 也比那时的功能强大。单元测试已经流行有一段时间了,简单的单元测试套件都可以为我们做构建提供更高的信心。

接下来,在过去某些项目中还出现过一种改进了的方式(坦白地说,我们近期没有看到过这么做的项目),即“rolling builds”过程,即持续不断地运行构建过程,而不是在夜间定时执行批处理过程。每当前一个构建完成之后,就立即从版本控制库中取得最新版本,进行下一次构建。在20世纪90年代初期,Dave使用过这种方式,取得了良好的效果。这种方式要比夜间构建好得多。但这种方式的问题在于,某次具体的提交与某次构建之间没有直接关联性。因此,尽管对开发人员来说,这样可以建立快速反馈环,然而很难追踪到是哪次提交令构建失败的,这在大型团队中尤其如此。

3.5 必不可少的实践

到目前为止,我们所说的都与构建和部署自动化相关。然而,这些自动化都是需要有人参与的。持续集成是一种实践,不是一个工具,它的有效性依赖于团队纪律。要让持续集成系统能够发挥作用,尤其是面对一个大型复杂的持续集成系统时,整个开发团队就必须有高度的纪律性。

持续集成系统的目标是,确保软件在任何时候都可以工作。为了做到这一点,下面是我们在自己的团队中使用的一些实践。我们之后还会讲述那些我们认为可选并推荐使用的实践,而这里列出的实践是持续集成发挥作用所必须的。

3.5.1 构建失败之后不要提交新代码

持 续集成的第一忌就是明知构建已经失败了,还向版本控制库中提交新代码。如果构建失败,开发人员应该尽快找出失败的原因,并修复它。假如使用这种策略,我们 每次都能非常迅速地找到失败原因并修复它。如果我们同事中的某人提交代码后使构建失败了,那么他们就是修复构建的最佳人选。此时,他们一定不希望别人再提 交新代码,因为那样的话,会触发新的构建,使问题越积越多。

一 旦这个规则被破坏,花更长的时间去修复是不可避免的。然后,大家就会习惯于看到构建失败,而且你很快就会发现,构建会一直处于失败状态而无人在意。这种状 态会一直持续下去,直到某个人忍无可忍,挺身而出,花相当大的气力再把它修好。但是,如果无人遵守规则,这个过程还会反复上演。所以,当把构建变绿之后, 最好借这个机会提醒每个人都遵守这个规则,确保构建一直是绿的,让软件一直处于可工作状态。

3.5.2 提交前在本地运行所有的提交测试,或者让持续集成服务器完成此事

正如之前提过的,我们希望每次提交都可以产生一个可发布的候选版本。任何人以任何形式公布某个东西之前,都会检查一下自己的工作成果,而候选版本也是一个发行物,所以每次提交前也要做一下检查。

我们希望提交过程是一件轻量级的事儿,这样就可以每隔二十分钟左右提交一次了,但它也应该是一件非常严肃的事儿,这样在每次提交之前,我们都会停一来,仔细想一想是否应该提交。提交前在本地运行一次提交测试,就是做一下健全性检查(sanity check)。它也让我们能确信新增的代码的确是按期望的方式运行的。

当开发人员准备提交时,应该从版本控制库中签出代码,更新一下本地的项目副本,然后做一下本地构建,并运行提交测试。只有当全部成功以后,开发人员才能将代码提交到版本控制库中。

如果以前未听说或使用过这种方法,你可能会问:“为什么在提交前还要运行本地提交测试呢?这样的话,我们的编译和提交测试不是要运行两次了吗?”这么做,有两个理由。

(1) 如果在你根据版本控制进行更新之前,其他人已经向版本控制库中提交了新代码,那么你的变更与那些新代码合并后,可能会导致测试失败。如果你 自己先在本地更新代码并运行提交测试的话,假如有问题,就会在本地提前发现,提前修复,从而不会令持续集成服务器上的构建失败,不至于影响其他人及时提 交。

(2) 在提交时经常犯的错误是,忘记提交那些刚刚新增加的东西到存储库中。如果遵守这个流程的话,当本地构建成功,而持续集成系统中的提交阶段失败了的话,那么你就知道要么是由于别人与你同时提交了代码,要么就是你遗漏了一部分类或配置文件没有提交到版本控制系统中。

遵循这样的实践可以确保构建状态一直是绿的。

很多现代持续集成服务器还提供这样一种功能,名字叫做预测试提交(pretested commit),也称为个人构建(personal build)或试飞构建(preflight build)。使用这种特性,就不必自己进行提交,持续集成服务器将拿到你的本地变更,把它放在构建网格中运行提交测试。一旦构建成功通过,持续集成 服务器就替你将变更提交到版本控制库中。如果构建失败的话,它会通知你哪里出错了。这是个非常不错的方法,即能遵守规则,又不需要坐在那儿等着提交测试通 过,而是直接开始做下一个任务。

在本书写作时,Pulse、TeamCity和 ElectricCommander这三种持续集成服务器都已经提供了这个功 能。如果使用分布式版本控制系统的话,这个实践就更容易了,因为你可以将代码存储到自己的本地代码控制库中,而无需提交到团队的中央版本控制库中。通过这 种方式,一旦个人构建失败的话,很容易通过创建补丁的方式将自己提交的修改搁置,恢复到你刚刚提交到持续集成服务器的那个版本上,将构建修复,再把补丁放 上去[④]。

3.5.3 等提交测试通过后再继续工作

持续集成系统是整个团队的共享资源。当一个团队有效地使用持续集成时,如果遵循我们的这些建议,频繁提交代码,那么对于整个项目和团队来说,构建失败就会成为一个小问题,很容易修复,不足道也。

当然,构建失败是持续集成过程中一个平常且预料之中的事情。我们的目标是尽快发现错误,并消灭它们,而不是期待完美和零错误。

在提交代码时,做出了这一代码的开发人员应该监视这个构建过程,直到该提交通过了编译和提交测试之后,他们才能开始做新任务。在这短短几分钟的提交阶段结束之前,他们不应该离开去吃午饭或开会,而应密切注意构建过程并在提交阶段完成的几秒钟内了解其结果。

如果提交阶段成功了,而且只有提交阶段成功之后,开发人员才能做下一项任务。如果失败了,他们就要着手发现问题的原因并修复它(要么提交新的代码去修复这个问题,要么回滚到原来的版本,即把这次不成功的代码从代码库中拿出来,把问题修复之后再重新提交)。

3.5.4 回家之前,构建必须处于成功状态

现在是星期五的下午五点半,同事们都走出公司大门了,而你刚刚提交了代码,让构建失败了。此时你有三个选择:(1) 晚一点儿回家,先把它修复了;(2) 将提交回滚,下周上班再提交;(3) 现在就走,不管那个失败的构建。

如 果没管那个失败的构建,当周一来上班时,你可能要花上一段时间来回忆上个星期五都做了哪些修改导致构建失败了并尝试修复。如果星期一早上你不是第一个来上 班的人,那么先到公司的人会先发现构建失败了,他们会对你的行为表示不满。假如你周末突然生病了,周一不能上班,那么你的同事就可能给你打上几通电话,问 你是怎么做的,如何修复它。他们也可能不管三七二十一,直接将你的修改回滚,但即使这样,你耳根还会发烧,因为他们还会嘀咕你的名字。

如果是在一个位于不同时区的分布式团队中工作,通常来说,失败构建的影响就更大,尤其是当一天工作结束时构建失败了却对其置之不理时。在这种情形下,让失败的构建过夜是疏远你远方同事最有效的方式。

在这里需要澄清一下,我们并不建议你工作到很晚来修复失败的构建,而是希望你有规律地尽早提交代码,给自己足够的时间处理可能出现的问题。或者,你可以第二天再提交。很多有经验的开发人员在下班前一小时 内不再提交代码,而是把它作为第二天早上的第一件事情。如果所有手段都不好使,那么把版本控制库中的代码回滚到上一次成功构建的状态,并在本地保留一份失 败的代码就可以了。一些版本控制工具(包括所有的分布式版本控制工具)可以让你非常轻松地在本地代码库中积累待提交的代码,而不会将其推送给其他团队成 员。

分布式项目中的构建纪律

我们曾经参与开发一个我们认为是当时世界上最大的敏捷项目。这个项目的人员分布在世界上的不同地点(美国的旧金山和芝加哥,英国的伦敦和印度的班加罗尔),而且共享同一个代码库。在一天24小时内只有三个小时的时间可能没有人提交代码。其他时间里,几乎每时每刻都有人提交代码,构建从来就没有停过。

如果位于印度的团队破坏构建后就回家了,那么位于伦敦的团队一整天的工作都会受到极大影响。同样,如果位于伦敦的团队做了同样的事,那么位于美国的同事可能在接下来的八小时之内一直在他们的阴影下工作。

严格的构建纪律是必须的,以致我们需要有一个专职的构建工程师,他不仅要维护构建,还要间或执行监管工作,确保破坏构建的人及时去修复它。如果修复不了的话,构建工程师有权直接将其回滚到上一次成功的状态。

3.5.5 时刻准备着回滚到前一个版本

正 如之前提到的,尽管我们努力做到最好,但还是会犯错误。每个人都可能破坏构建,这是预料之中的。在一个大型项目中,每天都可能发生这样的事,尽管预测试提 交在很大程度上可以缓解其发生的概率。此时,我们很容易认识到如何修复,而且可能只需要改一行代码。然而,有时候就没那么简单啦。要么是找不到引起问题的 根源,要么是提交之后才发现某部分非常重要的内容还没有做。

如 果某次提交失败了,无论采取什么样的行动,最重要的是尽快让一切再次正常运转起来。如果无法快速修复问题,无论什么原因,我们都应该将它回滚到版本控制库 中前一个可工作的版本上,之后再在本地环境中修复它。毕竟,我们使用版本控制系统的首要理由就是,它能让我们回滚任意操作而且不会丢失任何信息。

飞 行员被告诫每次飞机着陆时,都应该假定有出错的可能,并随时做好重新升空再次尝试降落的准备。当我们提交代码时也要有同样的思想准备,即假设提交的代码会 破坏构建,而且会需要较长时间来修复,要知道如何将提交回滚到版本库中某个已知的正确版本上。你应该很清楚在你提交前的那个版本是好的,因为持续集成的实 践之一就是不能在构建失败状态下提交代码。

3.5.6 在回滚之前要规定一个修复时间

建立一个团队规则:如果因某次提交而导致构建失败,必须在十分钟之内修复它。如果在十分钟内还没有找到 解决方案的话,就将其回滚到版本控制系统中前一个好的版本。如果团队能够忍受,有时候也可以延长一段时间来修复它。比如,你已经找到问题根源并修改了代 码,正在运行本地构建,如果它成功就可以提交了。在这个时候,可以等一等,看一下这次本地构建的结果。如果这次本地构建成功,你就能提交了,希望这次提交 能够修复持续集成服务器上的构建。然而,不管是本地构建,还是持续集成服务器的构建,只要它又失败了,我们就不再等待,直接将其回滚到前一次好的状态下。

有经验的开发人员都会愿意遵守这个规则,并愿意将十分钟内或更久还无法修复的版本从版本控制库中剔除。

3.5.7 不要将失败的测试注释掉

一旦你决定执行前面所说的规则,有些开发人员常常为了能够提交代码,而将那些失败的测试注释掉。这种冲 动是可以理解的,但却是无法被容忍的一种错误行为。那些已经成功运行了一段时间的测试失败时,失败的原因可能很难找。这种失败是否真的意味着发现了一个回 归问题呢?也许这个测试不再是有效的测试了,也许是因为原有功能因需求变化被改变了。找出真正的失败原因可能需要向很多人了解情况,并且需要花上一段时 间,但这是值得的。我们的选择是要么修复代码(如果是回归问题的话),要么修改测试(如果该测试以前的某个假设不成立了),或者删除它(如果被测试的功能 已经不存在了)。

将失败的测试注释掉应该是最后不得已的选择,除非你马上就去修改它,否则尽量不要这么做。偶尔注释掉一个测试是可以的,比如,当某个非常严重的问题需要解决,或者是某些内容需要与客户进一步探讨时。然而,这很可能让你滑入泥潭。我们曾遇到过一个情景,项目中50%的测试被注释掉了。所以,我们建议统计一下被注释掉的测试数量,并把它公示出来。我们可以设定一个限定值(比如测试总数的2%),一旦被注释掉的测试数量超过这个值,就让持续集成服务器上的构建自动失败。

3.5.8 为自己导致的问题负责

假如提交代码后,你写的测试都通过了,但其他人的测试失败了,构建结果还是会失败。通常这意味着,你引入了一个回归缺陷。你有责任修复因自己的修改导致失败的那些测试。在持续集成环境中这是理所当然的,但可惜的是,在很多项目中事实并不是这样的。

这一实践有多层含义。首先,你应该有权存取自己的更改可能破坏的所有代码。因为只有这样,当被破坏时你才能修复它。也就是说,不能让开发人员独立拥有某部分代码的修改权。为了持续集成更加有效,每个人都应该能够存取所有代码库。如果因为某种原因,无法保证这一点的话,可以通过保证所有人之间的良好沟通和协作达到这一点。但是,这是没有办法中的办法,你应该努力排除这种代码私有化的问题。

3.5.9 测试驱动的开发

对 于持续集成来说,全面的测试套件是非常必要的。虽然我们会在下一章详细讨论自动化测试,但还是应在这里先强调一下,只有非常高的单元测试覆盖率才有可能保 证快速反馈(这也是持续集成的核心价值)。完美的验收测试覆盖率当然也很重要,但是它们运行的时间会比较长。根据我们的经验,能够达到完美单元测试覆盖率 的唯一方法就是使用测试驱动开发。尽管我们尽量避免在本书中教条式地提及敏捷开发实践,但我们认为测试驱动开发是持续交付实践成为可能的关键。

这 里向不太熟悉测试驱动开发的读者简单介绍一下。所谓测试驱动开发是指当开发新的功能或修复缺陷时,开发人员首先要写一个测试,该测试应该是该功能的一个可 执行规范。这些测试不但驱动了应用程序的设计,而且即可以作为回归测试使用,也是一份代码的说明文档,描述了应用程序预期的行为。

关于测试驱动开发的话题超出了本书的范围。但值得注意的是,和所有其他此类实践一样,测试驱动开发也需要纪律性和实效性。在这里我们向读者推荐两本相关的书藉:Steve Freeman和Nat Pryce合著的Growing Object-Oriented Software, Guided by Tests, 以及Gerard Meszaros写的xUnit Test Patterns: Refactoring Test Code。

3.6 推荐的实践

下面的实践并不是必须的,但是我们认为比较有用,项目中应该给予考虑。

3.6.1 极限编程开发实践

持续集成是Kent Beck关于极限编程的书中描写的十二个核心实践之一,它与其他极限实践互为补充。对于任何团队,即使不采用其他实践,只用持续集成也会给项目开发 带来很大改善,而若与其他实践相结合的话,它的作用会更大。尤其是,除了测试驱动开发和我们前面讲到的代码集体所有权,你还应该考虑把重构作为高效软件开 发的基石。

重 构是指通过一系列小的增量式修改来改善代码结构,而不会改变软件的外部行为。通过持续集成和测试驱动开发可以确保这些修改不会改变系统的行为,从而使重构 成为可能。这样,你的团队就可以自由自在地修改代码,即使偶尔涉及较大范围的代码修改,也不用担心它会破坏系统了。这个实践也让频繁提交成为了可能,即开 发人员在每次做了一个小的增量式修改后就提交代码。

3.6.2 若违背架构原则,就让构建失败

开发人员有时很容易忘记系统架构的一些原则。我们曾经使用过一种手段来解决这个问题,那就是写一些提交时测试,用于证明这些原则没有被破坏。

这种技术很战术化,让我们举例说明。

在构建时执行远程调用

我们能想到的最好的例子是一个由很多个分布式服务组成的真正分布式系统,因为它在客户端要执行很多业务逻辑,而真正的业务逻辑在服务器端也有一些(这是为满足真正的业务需求,而不是劣质编程)。

我们的开发团队在其开发环境上同时为客户端系统和服务器端系统部署了所有代码。对于开发人员来说,很容易将这种环境中客户端和服务器端之间的通信变成本地调用,却没有意识到真正需要的是远程调用。

我们将代码设计成由多个包组成,每个包是我们分层模式中的一个边界面,以便于部署。同时我们还结合了一些可以评估代码依赖性的开源软件,并使用了grep查找依赖性分析工具的输出,来查看在这些包之间是否有依赖破坏了我们的架构原则。

这避免了在做功能测试时不必要的失败,并有助于强化系统架构,它可以提醒开发人员区分两个子系统边界的重要性。

这种技术看上去有点儿重量级的感觉,而且也无法取代开发团队对整个系统架构的清晰理解。可是,当需要严格保护我们的架构时,这种方法就非常有用,否则很难在早期发现破坏架构的那类问题。

3.6.3 若测试运行变慢,就让构建失败

前 面提到过,持续集成需要小步频繁提交。如果提交测试要运行很长时间的话,这种长时间的等待会严重损害团队的生产效率,他们将花费很长的时间等待构建和测试 过程完成。而且,这样也无法做到频繁提交,结果会导致团队成员开始把每次要提交的内容都存在本地,而每多增加一次本地保存就会增加一些复杂性,同时也增加 了与版本控制库的代码出现合并冲突的可能性,增加了引入错误的几率,最终可能导致测试失败。所有这些最终都会导致生产率下降。

为了让开发团队注意到快速测试的重要性,可以这样做:当某个测试运行超过一定时间后,就让这次提交测试失败。我们在上一个项目中使用的这一时间是两秒。

我 们喜欢那种只需很小的改变就能带来很大效果的实践,而该实践就属于这种。如果开发人员写了一个需要较长时间运行的提交测试,当他提交时,这次提交构建就会 失败。这会鼓励开发人员仔细思考如何让测试运行得更快。测试运行得越快,才有可能更频繁地提交。当开发人员提交频率高了,遇到合并问题的可能性就减少了, 而且即使有问题,也不会是大问题,而且很快就能解决,那么开发人员的生产率也就提高了。

这 里还要补充一点,这个实践是一柄“双刃剑”。在创建测试时要谨防那种不强壮的测试(比如,当持续集成环境由于某种原因出现不寻常的负载时,该测试就罢工 了)。我们发现,使用这种方法最好就是把它作为一种让大团队聚焦于某个具体问题的策略,而不是作为每次构建都要用到的手段。如果构建速度慢,可以用这种方 法让团队暂时关注于提高速度。

请注意,我们谈的是测试本身的性能问题,而不是有关性能测试的问题。容量测试会在第9章描述。

3.6.4 若有编译警告或代码风格问题,就让测试失败

编译器发出警告时,通常理由都足够充分。我们曾经用过一个比较成功的策略,即只要有编译警告,就让构建失败,但我们的开发团队常常把它叫做“纳粹代码”。这在某些场合可能有点儿苛刻,但作为强迫写好代码的一种实践,还是很有效的。

你可以通过添加代码检查尽可能地强化这一技术。我们成功使用过很多关于代码质量检查的开源工具,如下所示:

Simian是一种可以识别大多数流行语言(包括纯文本)中重复代码的工具。

JDepend是针对Java的免费版本,它有一个.NET的商业版本NDepend,它们拥有大量对设计质量进行评估的实用(和不太实用)的度量指标。

CheckStyle可以对“烂代码”做一些检查,比如工具类中的公共构造函数、嵌套的代码块和比较长的代码行。它也能找到缺陷和安全漏洞的一些常见根源。它还很容易被扩展。FxCop是它的.NET版本。

FindBugs是一个Java软件,它是CheckStyle的替代品,有一些相似的校验功能。

正如我们所说,对于那些有编译警告就让构建失败的项目来说,可能的确有点苛刻。我们通常会渐进式地引入这种实践,即将编译警告的个数或者是TODOs的个数与前一次提交中的个数进行比较。如果个数增加,我们就让构建失败。通过这种方法,就可以比较容易地执行下面的规则:每次提交都应该减少警告或TODOs的个数,至少减少一个。

CheckStyle:挑刺儿是值得的

在一个项目里,我们把CheckStyle加到了提交测试集合中。尽管团队里怨声载道,但毕竟都是些有经验的开发人员,我们一致认为在一段时间内承担这种痛苦是值得的,因为它会让我们养成良好的习惯,并让项目有一个良好的基础。

项目进行几个星期之后,我们移除了CheckStyle。这让构建速度提高了,并且不再有CheckStyle带来的痛苦。然而,随着更多人员加入到团队中,几个星期后,我们发现代码中的“坏味道”开始增加了,重构中要花较长的时间来清理这些坏代码。

最后,我们认识到,尽管使用CheckStyle会付出一些代价,但有助于让团队理解普通代码与高质量代码的区别。我们把CheckStyle又放到了提交测试集合中,并不得不花一些时间来修复因此导致的一些小问题。但是,至少对这个项目来说,这是值得的,并且我们的团队面对问题时渐渐不再抱怨,而是想方法解决它。

3.7 分布式团队

单单就流程和技术而言,分布式团队中使用持续集成与在其他环境中没有什么大的分别。但是,团队成员不能坐在同一间屋子里工作(他们甚至可能身处不同的时区),的确在某些方面会有影响。

从技术角度上看,最为简单的方法(也是从流程角度上讲最有效的方法)就是使用共享的版本控制系统和持续集成系统。如果项目中使用了后面几章将提到的部署流水线,那么共享的版本控制系统和持续集成系统应在人人平等的基础上,对团队的所有成员可用。

当说这个方法最有效时,我们是想强调它是很值得考虑的,而且也是值得付出努力达到这种理想状态的。此处讲述的其他方法的效果都远不如这个方法。

3.7.1 对流程的影响

对 在同一时区内的分布式团队来说,持续集成流程基本是一样的。当然,你无法以实物的形式使用提交令牌。虽然有些持续集成服务器支持虚拟令牌,但它不具有人性 化特点,所以当你提醒某人去修复构建时,容易导致大家的抵触心理。同时,类似“个人构建”这种功能会变得更加有用。但总地来说,流程是一样的。

对分布在不同时区的分布式团队来说,就需要多处理一些事情啦。如果在旧金山的团队在破坏构建以后回家了,那么,这对北京的团队可能就是个严重的阻碍。因为当旧金山的团队下班后,北京才刚上班。尽管流程没有什么变化,但不良影响会被放大。

对于开发大型项目的分布式团队,像Skype这样的VoIP工具和即时消息工具(IM)对于展开细粒度的沟通,顺利开展项目工作是非常重要的。与开发有关的人(项目经理、业务分析师、开发人员和测试人员)互相之间都应该能利用IM和VoIP进行即时沟通。并且为了使交付过程更加平稳,让各团队之间的人员做定期轮换也是非常必要的,这样每个地方的成员都能与其他地方的团队成员建立起一些私人交情。对于建立团队成员间的信任来说,这是非常重要的,也是分布式团队中最先要面对的问题。通过视频会议设备进行回顾会议、展示会、站立会议和其他常规会议也是可行的。还有一种不错的技术,就是让每个开发团队使用屏幕录像软件录制一下他们在当天所开发的功能。

显然,这是一个比持续集成更广泛的话题。我们的主要观点是让整个流程保持一致,甚至要具有更加严格的纪律性。

3.7.2 集中式持续集成

一 些功能更强大的持续集成服务器提供像“集中管理构建网格”和“高级授权机制”这种功能,用于把持续集成作为一个集中式服务,为大型分布式团队提供服务。这 样的服务器让团队很容易建立自服务式的持续集成服务,而不需要自己管理硬件。它也会让运维团队将持续集成作为集中式服务,统筹服务器资源,管理持续集成和 测试环境的配置,以确保这些环境的一致性以及与生产环境的相似性,还能巩固一些好的实践,比如第三方库的配置管理,预安装一些工具(用于收集代码覆盖率和 质量的统一度量数据。最终,我们可以做到项目之间的统一度量数据的收集和监控,为管理者和交付团队提供程序级的代码质量监控方式。

虚 拟化技术可以与集中式持续集成服务很好地结合,只需要单击一下按钮就能利用已保存好的基线镜像重建一个新的虚拟机。利用虚拟化技术,可以为开发团队提供一 键式搭建新环境这样的自服务功能。这也可以确保构建和部署一直运行在一致的基线版的环境中。人们常常说“持续集成环境是一种艺术作品”,这是因为持续集成 环境经过几个月的积累后,安装了很多软件、库文件,进行了很多种配置,让人根本不知道哪些与测试环境有关,哪些与生产环境有关,而虚拟化技术恰好能够解决这样的问题。

集 中式持续集成是一种双赢实践。然而,为了达到这种双赢状态,开发团队必须能够很容易地通过自动化方式创建环境,并进行自动配置、构建和部署。当某个团队在 准备持续集成环境时,还要发送几封电子邮件并再等上几天的话,他们就会违反这一约定,重新回到原来的手工方式,即在自己的桌上找到一台空闲机器,自己做持 续集成,或者更糟,他们根本不做持续集成了。

3.7.3 技术问题

当分布于世界各地的团队之间网络状况不佳时,依据选择的不同版本控制系统,团队间共享版本控制系统、构建和测试资源的做法有时候也会有很多麻烦。

在 持续集成运转良好时,整个团队都会有规律地提交代码。这意味着,与版本控制系统之间的交互通常保持在一个较高的合理水平上。由于提交和更新比较频繁,虽然 每次交互通常都较小(甚至可以用字节来计算),劣质的通信仍会严重拖生产效率的后腿。因此,加大投入在各开发中心之间建立起足够高带宽的通信机制是非常必 要的。考虑将集中式的版本控制库迁到某种分布式版本控制系统(比如Git或Mercurial)也是不错的选择。闻名知意,即使无法连接到主服务器,分布式版本控制系统也能让大家提交代码。

分布式版本控制:最后的选择

几年前我们做过一个项目,当时就遇到了这样的问题。与印度同事沟通的网络基础设施非常慢,并且不稳定。 有几天在印度的同事甚至根本无法提交代码,这在后来的几天内引起了一系列的连锁反应,后果可想而知。后来,我们做了时间成本分析,发现升级通信用的基础设 施也就是几天的事情。在另外一个项目中,我们根本无法得到足够快且稳定的网络连接。最后,团队将版本控制库从Subversion(一个集中式版本控制系统)换成了Mercurial(一个分布式版本控制系统),生产效率得到了显著提升。

版本控制系统应该与那些运行自动化测试所用的构建基础设施在网络连接上近一些。因为,如果每次提交后都运行这些测试,这两者之间的网络交互是相当多的。

任 何一个开发中心都应该能在平等的基础上,访问那些运行有部署流水线中的版本控制系统、持续集成系统以及各种测试环境的机器。假如由于磁盘已满而导致在印度 的版本控制系统无法工作,而且印度同事此时都下班回家了,但是伦敦的开发团队却又无法登录到印度这台版本控制系统上进行清理的话,这对伦敦的开发团队来 说,无疑会有严重影响。所以,一定要为每个地点的团队都提供所有系统的系统级访问权限,确保任何每个开发站的团队不但可以访问,而且可以修正那些与其换班相关的问题。

3.7.4 替代方法

如果由于某些不可克服的原因,无法再增加投入在开发中心间建立更高带宽的通信机制,各地团队还可以使用 本地持续集成和测试系统(当然这不太理想),甚至在某些极端情况下,不得不用本地的版本控制系统。我们并不建议使用这种方法,但这种情况在现实中还是很有 可能的。所以,我们要尽一切可能避免使用这种方法。这种方法在时间和人力上的成本都很高,而且根本无法做到团队间的共享访问和控制。

比较容易解决的是持续集成系统。我们可以用本地持续集成服务器和测试环境,甚至全套的本地部署流水线, 尤其是当其中的某个团队需要做大量的手工测试时。当然,我们需要小心地管理这些环境,以确保它们在各团队之间都是一致的。唯一需要说明的一点是,二进制文 件或安装文件最好只构建一次。当每个团队需要这些文件时,都要从同一处获取同一份副本。然而,大多数情况下,安装包的尺寸都比较大,此时这种做法就有些不 太现实了。如果不得不在本地构建二进制文件或安装包,那么更有必要保证所有配置都是严格一致的,以便确保无论在哪儿的构建结果都是完全一致的。确保这一点的一种方法是利用MD5或相似的算法为二进制文件自动生成散列,并让持续集成服务器自动将它们的散列与“原始”二进制文件的散列相比较,以确保其没有差别。

在某些极端情况下,比如在分布式开发中版本控制服务是远程连接的,而网络慢且不稳定时,那么在本地建立 一套持续集成系统就显得非常必要了。我们经常说,使用持续集成的目的就是能够更早发现问题。假如版本控制系统是分治的(无论是以哪种方式分治),识别问题 的能力都会打折扣。假如一定要这么做的话,在这种版本控制系统分治的情况下,我们的目标就是将从问题被引入的时间点到它被发现的时间点之间的时间最小化。

对于分布式团队来说,主要有两种方式来解决本地化版本控制系统的存取问题:一是将应用程序分成多个组件;二是使用那些分布式或支持多主库拓扑结构的版本控制系统。

对于基于组件的方法,可以根据组件或功能边界来划分版本控制库和团队。我们会在第13章详细讨论这种方法。

我们还看到过另一种方法,既有本地团队代码库,又有使用全球共享主库的构建系统。根据功能划分的团队在 工作日将其代码提交到他们自己的本地代码库中。在每天的某一时间点上,通常是其他时区的某个分布式团队完成了一天的工作时,本地团队的某人将当天本团队所 有修改一并提交到主库中,这个人要负责合并所有的修改集。显然,使用分布式版本控制系统会让这项工作容易得多,因为分布式版本控制系统就是为这种工作方式 设计的。可是,这种解决方案也绝不是非常理想的方案,我们看到过这种方案的失败案例,原因是有太多的合并冲突问题。

总之,本书中所描述的所有技术在很多项目中已被分布式团队所验证。我们认为,在分布于不同地理位置的团队能够有效合作的重要因素中,持续集成算是仅有的两三种最重要因素之一。持续集成中的“持续”是很重要的。如果真的无从选择,与其使用一些“权宜之计”,倒不如将花一些钱在通信带宽上,从中长期来说,这是比较经济实惠的。

3.8 分布式版本控制系统

DVCS(Distributed Version Control System, 分布式版本控制系统)的兴起是团队合作方式的革命性改进。很多开源项目曾经使用电子邮件或论坛发帖的方式来提交补丁,而像Git和Mercurial这种工具让开发人员之间、团队之间以及分支与合并工作流时的打补丁变得极其简单。DVCS使你能够离线工作、本地提交,或在将修改提交给其他人之前把这些代码搁置起来或对其做rebase操作。DVCS的核心特性是每个仓库都包括项目的完整历史,这意味着除了团队约定之外,仓库是没有权限控制功能的。所以,与集中式系统相比,DVCS引入了一个中间层:在本地工作区的修改必须先提交到本地库,然后才能推送到其他仓库,而更新本地工作区时,必须先从其他仓库中将代码更新到本地库。

DVCS为协作提供了新的强有力的方法。比如,GitHub就是为开源项目提供这种新型协作方式的先行者。在传统方式中,提交人(committer)扮演着项目代码库守门人的角色,可以接受或拒绝贡献者(contributor)的补丁。当两个贡献者的补丁互不相容时,就会出现分支(fork)。在GitHub模式中,这种情况最终得以扭转。如果对某个开源项目做贡献,首先单击项目站点的“fork”按钮,创建它的一个分支库,并对该分支进行一些修改,然后再让原始代码库的所有者从你的代码库中将修改取出(pull)并合并到原始代码库。在那些较活跃的项目里,分支的网状结构会激增,每个分支都可能新增了不同的特征集。偶尔这些分支会有一些分歧。这种模式的动态性比传统模型强得多,通常来说,在传统模式中补丁并不多,而且在长长的邮件列表中很容易被忽略掉。可是在GitHub上,贡献者更多,因此开发节奏也会随之加快。

可是,这种模式挑战了持续集成的一个基本假设,这个假设就是:存在代码的单一权威版本(通常称作主干,即mainline或者trunk),所有的修改都会提交到这个主干上。可我们要说的是,使用DVCS后,你还可以使用版本控制的主干模式(mainline model)很好地做持续集成。只要你指定某个仓库作为主库(master),每次更改这个仓库就触发持续集成服务器上的一次构建,并让每个人都将其修改推送到这个仓库中来实现共享。很多使用分布式系统的项目都使用这种方式,而且非常成功。它保留了DVCS的很多优点,比如可以频繁提交更改而不用将更改共享给其他人(就像保存游戏进度一样),当尝试不同的方法开发新功能或做一系列复杂的重构时,这个特点非常有用。然而, DVCS的某些使用模式对于持续集成有阻碍作用。比如,GitHub模式干扰了代码共享的主线模型(mainline/trunk model),因此也阻碍了真正的持续集成。

在GitHub中,每个用户的变更集合都放在不同的代码库中,很难说出哪些用户的哪些变更会被成功集成。但是,你可以再创建一个代码库,用来监控其他所有 的代码库,一旦其中任何一个代码库发生了变化,就尝试将它们全部合并在一起。然而,在合并时常常会因冲突而失败,更不用说运行自动化测试啦。而且,随着贡 献者和代码库数量的增加,问题也会成指数级增长。最终可能没人会留意持续集成服务器所传达的信息,这样持续集成作为沟通“当前应用程序是否能够工作(如果 不能正常工作,那么是谁或什么原因使然)”的手段就失败了。

当然,利用更简单的模式让持续集成为我们提供一些益处也是可行的。比如,可以为每个代码库都在持续集成服务器上建立一个构建。每次向该代码库提交代码时,就尝试让它与指定的主库合并,并运行构建。图3-2展示的是利用CruiseControl.rb,对带有两个分支的项目Rapidsms构建主代码库。

enter image description here

为了创建这样的系统,可以通过命令git remote add core git://github.com/ rapidsms/rapidsms.git将指向项目主代码库的一个分支加到每个CC.rb所监控的Git代码库中。每次触发构建时,CC.rb都会尝试合并且运行构建:

git fetch core

git merge --no-commit core/master

[command to run the build]

这个构建之后,CC.rb会运行命令git reset –hard来重新将本地存储库指向Head。这种方式并不是真正的持续集成,可是的确可以告诉分支的维护者(以及主仓库的维护者),他们的分支在原则上是否能合并到主仓库中,以及合并之后该应用程序是否还可以正常工作。有趣的是,图3-2显示出主代码库的构建当前是失败的,但分支Dimagi不但与其合并成功,而且还修复了被破坏的测试(还有可能自行增加了一些功能)。

持续集成再向前一步,就是Martin Fowler所说的“随性集成”(promiscuous integration)[bBjxbS]。在这种模式下,贡献者不仅在分支和中心代码库之间取代码,还会从分支之间取代码。这种模式在使用GitHub上 那些较大的项目中比较常见,当一些开发人员正工作在某个生命周期比较长的特性分支上时,会从该特性分支的其他分支上取有变更的代码。在这种方式下,甚至不 需要任何有权限控制的代码库。软件的一个发布版本可能来自于任意一个分支,只要它通过了所有的测试并被项目经理所认可。这种模型为分布式版本控制系统作出 了合理的解释。

持续集成的以上这些替代方案可以创建高质量可工作的软件。然而,这必须满足以下条件才能成为事实。

有一个成员比较少,但都非常有经验提交团队。他们可以取每个补丁、照管自动化测试并确保软件的质量。

频繁地从分支上取被修改过的代码,以避免由于积累太多的代码使变更很难合并。如果发布的时间计划非常严格,则这个条件就非常重要,因为人们倾向于临在近发布时刻再合并,而此时的合并是极其痛苦的──这正是持续集成要解决的问题。

相对较少的核心开发人员,可能有一个贡献频率较低但人员较多的社区作为补充。这会让合并具有可追溯性。

这些条件通常比较适合大多数开源项目和规模相对较小的团队,但在大中型全职开发人员的团队中却非常少见。

总而言之,分布式版本控制系统代表了一次巨大的进步,为协同工作提供了有效的工具,无论你是否在开发一个分布式项目。作为传统持续集成系统(即有一个专属中央代码库,并且每个人都向其频繁地提交代码,且至少每天提交一次)的一部分,DVCS可说是极其有效。它也可以应用于不具备条件做持续集成的模式中,但对于交付软件来说,它仍旧会产生促进作用。但是,如果不能满足上述几个条件,我们反对使用这些模式。第14章会全面讨论这些以及其他模式,以及在什么样的条件下它们才会发挥作用。

3.9 小结

如果本书所介绍的开发实践里,你只想选择其中一种的话,我们建议你选择持续集成。我们一次又一次地看到该实践提高了软件开发团队的生产率。

持续集成的使用会为团队带来一种开发模式上的转变。没有持续集成的话,直到验证前,应用程序可能一直都处于无法工作的状态,而有了持续集成之后,应用程序就应该是时刻处于可工作状态的了,虽然这种自信取决于自动化测试覆盖率。持续集成创建了一个快速的反馈环,使你能尽早地发现问题,而发现问题越早,修复成本越低。

持续集成的实施还会迫使你遵循另外两个重要的实践:良好的配置管理和创建并维护一个自动化构建和测试流程。对某些团队来说,这一目标可能看起来遥不可及,但完全可以逐步达到。我们在前一章已经讨论过实现一个良好配置管理的步骤。第6章会讨论更多的构建自动化,而在下一章中,我们会更详细地介绍测试。

显 然,持续集成需要良好的团队纪律提供支持。事实上,哪种流程不需要纪律呢?其不同之处在于,有了持续集成之后,就有了一个“该纪律是否被严格遵守”的信息 指示器:构建应该保持在常绿状态。假如发现构建是绿的,而大家却并没有足够地遵守纪律,比如没有达到单元测试覆盖率,你就能非常容易地将各种检查加入到持 续集成系统中,强制团队养成良好的行为习惯。

总之,一个好的持续集成系统是基石,在此之上你可以构建更多的基础设施:

一个巨大的可视化指示器,用于显示构建系统所收集到的信息,以提供高质量的反馈;

结果报告系统,以及针对自己测试团队的安装包;

为项目经理提供关于应用程序质量的数据的提供程序;

使用部署流水线,可以将其延展到生产环境,为测试人员和运维团队提供一键式部署系统。