很多软件项目都有一个非常奇怪而又常见的特征,即在开发过程里,应用程序在相当长的一段时间内无法运行。在那些分支生命周期很长或者直到最后才做验收测试的项目里尤其如此。可是,我们也看到,某些项目即便最新提交的代码破坏了已有功能,最多也只要几分钟就可修好。其不同之处在于后者使用了持续集成。持续集成要求每当有人提交代码时,就对整个应用进行构建,并对其执行全面的自动化测试集合。
持续集成是一种根本的颠覆。如果没有持续集成,你开发的软件将一直处于无法运行状态,直至(通常是测试或集成阶段)有人来验证它能否工作。有了持续集成以后,软件在每次修改之后都会被证明是可以工作的(假如有足够全面的自动化测试集合的话)。即便它被破坏了,你也很快就能知道,并可以立即修复。高效使用持续集成的那些团队能够比那些没有使用它的团队更快地交付软件,且缺陷更少。在交付过程中,缺陷被发现得越早,修复它的成本就越低,因此也就大大节省了成本和时间。
“持续集成”这一实践并非信手拈来,它需要有一定的先决条件。我们先介绍这些先决条件,然后再看一看有哪些工具可以利用。也许最重要的一点是,“持续集成”依赖于那些能够遵守一些重要实践的团队,所以我们也会花上一点时间来讨论一下。
在开始做持续集成之前,你需要做三件事情。
版本控制
与项目相关的所有内容都必须提交到一个版本控制库中,包括产品代码、测试代码、数据库脚本、构建与部署脚本,以及所有用于创建、安装、运行和测试该应用程序的东西。
自动化构建
你要能在命令行中启动构建过程。无论是通过命令行程序启动IDE来构建应用程序,然后再运行测试,还是使用多个复杂的构建脚本通过互相调用的方式来完成都行,但无论采用哪种机制,必须满足如下条件:人和计算机都能通过命令行自动执行应用的构建、测试以及部署过程。
现在,集成开发环境和持续集成工具的功能都非常强大。通常不需要切换到命令行,你就可以用集成开发环境完成应用程序的构建,并执行测试。然而,我们仍认为,你仍然需要有能力通过命令行执行,而不需要使用集成开发环境的构建脚本。对于这一点,可能存在一些争议,但我们的理由如下。
团队共识
持续集成不是一种工具,而是一种实践。它需要开发团队能够给予一定的投入并遵守一些准则,需要每个人都能以小步增量的方式频繁地将修改后的代码提交到主干上,并一致认同“修复破坏应用程序的任意修改是最高优先级的任务”。如果大家不能接受这样的准则,则根本无法如预期般通过持续集成提高质量。
为了做持续集成,你不一定就需要一个持续集成软件,正如我们所说,它是实践,并不是工具。
事实上,现在的持续集成工具其安装和运行都极其简单。有几个开源工具可供选择,比如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,它们都可以用于简单的持续集成
假如能够满足前面所述的先决条件,那么当你选择并安装好持续集成工具之后,只要再花几分钟的时间配置一下就可以工作了。这些配置包括让它知道到哪里寻找源代码控制库,必要时运行哪个脚本进行编译,并执行自动化提交测试,以及一旦最新的提交破坏了应用程序,通过哪种方式通知你。
接下来要让所有人开始使用这个持续集成服务器。下面是一个简单的过程。
一旦准备好要提交最新修改代码时,请遵循如下步骤。
如果团队中的每个人在每次提交代码时都能够遵循这些简单的步骤,你就可以很有把握地说:
“只要是在与持续集成一模一样的环境上,我的软件就可以工作。”
持续集成不会独立地帮你修复构建过程。事实上,如果你在项目中期才做这件事的话,可能会非常痛苦。为了使持续集成能够更有效,开始之前,你应该先做好下面这些事情。
对于持续集成来说,我们最重要的工作就是频繁提交代码到版本控制库。每天至少应该提交几次代码。
定期地将代码提交到代码主干上会给我们带来很多其他好处。
我们特意提到过“要提交到主干”。很多项目使用版本控制中的分支技术来进行大型团队的管理。然而,当使用分支时,其实不可能真正地做到持续集成。因为如果你在分支上工作,那么你的代码就没有和其他开发人员的代码进行即时集成。那些使用长生命周期分支的团队恰恰面临着我们在本章开始时描述的集成问题。除一些很有限的情况外,我们不推荐使用分支。
如果没有一系列全面的自动化测试,那么构建成功只意味着应用程序能够编译并组装在一起。虽然对于某些团队来说,这已经是非常大的一个进步了,但是,假如能够有一定程度的自动化测试,会让你更有信心说:“我们的应用程序是可以工作的。”自动化测试有很多种,我们会在下一章详细讨论。其中有三类测试我们会在持续集成构建中使用,它们分别是单元测试、组件测试和验收测试。
通过组合使用这三类测试,你就能确信引入的修改不会破坏任何现有功能。
如果代码构建和单元测试的执行需要花很长时间的话,你会遇到一些麻烦,如下所示。
理想情况下,提交前的预编译和测试过程,以及持续集成服务器上的编译和测试过程应该都能在几分钟内结束。我们认为,十分钟是一个极限了,最好是在五分钟以内,九十秒内完成是最理想的。
然而,有时候需要将测试分成几个阶段,如第5章所述。那么如何划分阶段呢?首先将其分成两个阶段。
将验收测试按功能块进行分组通常是可取的。这样,当仅修改了系统中的个别功能块时,就可以单独运行影响系统这部分功能的验证测试。很多单元测试框架都提供这样的分组功能。
有时候,你会遇到这种情况,项目由几个模块组成,而每个模块的功能相对独立。此时需要认真考虑如何在版本控制库和持续集成服务器上合理地组织这些模块。
对于保证开发人员的开发效率与明晰思路来说,开发环境的管理是特别重要的。当开发人员刚开始新任务时,应该总是从一个已知正确的状态开始。他们应该能够运行构建、执行自动化测试,以及在其可控的环境上部署其开发的应用程序,通常是在他们自己的开发机上。只有在特殊的情况下,才应使用共享环境开发。在本地开发环境上运行应用程序时,应确保所使用的自动化过程与持续集成环境中的一致,与测试环境中也是一样的,且生产环境中也是一样的
对第三方依赖的配置管理,即那些开发中所用的库文件和组件。应确保库文件或组件的版本都是正确的,即它们的版本与你正在开发的源代码的版本是相互匹配的。
当今市场上有很多产品可以提供针对自动化构建和测试过程的基础设施。持续集成工具最基本的功能就是轮询版本控制系统,查看是否有新的版本提交,如果有的话,则签出最新版本的软件,运行构建脚本来编译应用程序,再运行测试,最后将运行结果告知你。
这里可能还有包含触发器功能的版本控制系统,例如gitlib的钩子,能触发jenkins执行相关任务
本质上,持续集成软件包括两个部分
到目前为止,我们所说的都与构建和部署自动化相关。然而,这些自动化都是需要有人参与的。持续集成是一种实践,不是一个工具,它的有效性依赖于团队纪律。要让持续集成系统能够发挥作用,尤其是面对一个大型复杂的持续集成系统时,整个开发团队就必须有高度的纪律性。
持续集成系统的目标是,确保软件在任何时候都可以工作。为了做到这一点,下面是我们在自己的团队中使用的一些实践。我们之后还会讲述那些我们认为可选并推荐使用的实践,而这里列出的实践是持续集成发挥作用所必须的。
持续集成的第一忌就是明知构建已经失败了,还向版本控制库中提交新代码。如果构建失败,开发人员应该尽快找出失败的原因,并修复它。假如使用这种策略,我们每次都能非常迅速地找到失败原因并修复它。如果我们同事中的某人提交代码后使构建失败了,那么他们就是修复构建的最佳人选。
提交前在本地运行一次提交测试,就是做一下健全性检查(sanity check)。它也让我们能确信新增的代码的确是按期望的方式运行的。
当开发人员准备提交时,应该从版本控制库中签出代码,更新一下本地的项目副本,然后做一下本地构建,并运行提交测试。只有当全部成功以后,开发人员才能将代码提交到版本控制库中。
这么做,有两个理由。
很多现代持续集成服务器还提供这样一种功能,名字叫做预测试提交(pretested commit),也称为个人构建(personal build)或试飞构建(preflight build)。使用这种特性,就不必自己进行提交,持续集成服务器将拿到你的本地变更,把它放在构建网格中运行提交测试。一旦构建成功通过,持续集成服务器就替你将变更提交到版本控制库中。
当然,构建失败是持续集成过程中一个平常且预料之中的事情。我们的目标是尽快发现错误,并消灭它们,而不是期待完美和零错误。
在提交代码时,做出了这一代码的开发人员应该监视这个构建过程,直到该提交通过了编译和提交测试之后,他们才能开始做新任务。
如果提交阶段成功了,而且只有提交阶段成功之后,开发人员才能做下一项任务。如果失败了,他们就要着手发现问题的原因并修复它(要么提交新的代码去修复这个问题,要么回滚到原来的版本,即把这次不成功的代码从代码库中拿出来,把问题修复之后再重新提交)。
现在是星期五的下午五点半,同事们都走出公司大门了,而你刚刚提交了代码,让构建失败了。此时你有三个选择:
如果没管那个失败的构建,当周一来上班时,你可能要花上一段时间来回忆上个星期五都做了哪些修改导致构建失败了并尝试修复。如果星期一早上你不是第一个来上班的人,那么先到公司的人会先发现构建失败了,他们会对你的行为表示不满。假如你周末突然生病了,周一不能上班,那么你的同事就可能给你打上几通电话,问你是怎么做的,如何修复它。他们也可能不管三七二十一,直接将你的修改回滚,但即使这样,你耳根还会发烧,因为他们还会嘀咕你的名字。
如果是在一个位于不同时区的分布式团队中工作,通常来说,失败构建的影响就更大,尤其是当一天工作结束时构建失败了却对其置之不理时。在这种情形下,让失败的构建过夜是疏远你远方同事最有效的方式。
如果某次提交失败了,无论采取什么样的行动,最重要的是尽快让一切再次正常运转起来。如果无法快速修复问题,无论什么原因,我们都应该将它回滚到版本控制库中前一个可工作的版本上,之后再在本地环境中修复它。
毕竟,我们使用版本控制系统的首要理由就是,它能让我们回滚任意操作而且不会丢失任何信息。
建立一个团队规则:如果因某次提交而导致构建失败,必须在十分钟之内修复它。如果在十分钟内还没有找到解决方案的话,就将其回滚到版本控制系统中前一个好的版本。如果团队能够忍受,有时候也可以延长一段时间来修复它。
有经验的开发人员都会愿意遵守这个规则,并愿意将十分钟内或更久还无法修复的版本从版本控制库中剔除。
一旦你决定执行前面所说的规则,有些开发人员常常为了能够提交代码,而将那些失败的测试注释掉。这种冲动是可以理解的,但却是无法被容忍的一种错误行为。那些已经成功运行了一段时间的测试失败时,失败的原因可能很难找。
将失败的测试注释掉应该是最后不得已的选择,除非你马上就去修改它,否则尽量不要这么做。
假如提交代码后,你写的测试都通过了,但其他人的测试失败了,构建结果还是会失败。通常这意味着,你引入了一个回归缺陷。你有责任修复因自己的修改导致失败的那些测试。在持续集成环境中这是理所当然的,但可惜的是,在很多项目中事实并不是这样的
这一实践有多层含义。首先,你应该有权存取自己的更改可能破坏的所有代码。因为只有这样,当被破坏时你才能修复它。也就是说,不能让开发人员独立拥有某部分代码的修改权。为了持续集成更加有效,每个人都应该能够存取所有代码库。如果因为某种原因,无法保证这一点的话,可以通过保证所有人之间的良好沟通和协作达到这一点。但是,这是没有办法中的办法,你应该努力排除这种代码私有化的问题。
对于持续集成来说,全面的测试套件是非常必要的。只有非常高的单元测试覆盖率才有可能保证快速反馈(这也是持续集成的核心价值)。完美的验收测试覆盖率当然也很重要,但是它们运行的时间会比较长。根据我们的经验,能够达到完美单元测试覆盖率的唯一方法就是使用测试驱动开发。尽管我们尽量避免在本书中教条式地提及敏捷开发实践,但我们认为测试驱动开发是持续交付实践成为可能的关键。
所谓测试驱动开发是指当开发新的功能或修复缺陷时,开发人员首先要写一个测试,该测试应该是该功能的一个可执行规范。这些测试不但驱动了应用程序的设计,而且既可以作为回归测试使用,也是一份代码的说明文档,描述了应用程序预期的行为。
下面的实践并不是必须的,但是我们认为比较有用,项目中应该给予考虑。
持续集成是Kent Beck关于极限编程的书中描写的十二个核心实践之一,它与其他极限实践互为补充。对于任何团队,即使不采用其他实践,只用持续集成也会给项目开发带来很大改善,而若与其他实践相结合的话,它的作用会更大。尤其是,除了测试驱动开发和我们前面讲到的代码集体所有权,你还应该考虑把重构作为高效软件开发的基石。
重构是指通过一系列小的增量式修改来改善代码结构,而不会改变软件的外部行为。通过持续集成和测试驱动开发可以确保这些修改不会改变系统的行为,从而使重构成为可能。这样,你的团队就可以自由自在地修改代码,即使偶尔涉及较大范围的代码修改,也不用担心它会破坏系统了。这个实践也让频繁提交成为了可能,即开发人员在每次做了一个小的增量式修改后就提交代码。
开发人员有时很容易忘记系统架构的一些原则。我们曾经使用过一种手段来解决这个问题,那就是写一些提交时测试,用于证明这些原则没有被破坏。
这种技术看上去有点儿重量级的感觉,而且也无法取代开发团队对整个系统架构的清晰理解。可是,当需要严格保护我们的架构时,这种方法就非常有用,否则很难在早期发现破坏架构的那类问题。
如果提交测试要运行很长时间的话,这种长时间的等待会严重损害团队的生产效率,他们将花费很长的时间等待构建和测试过程完成。
为了让开发团队注意到快速测试的重要性,可以这样做:当某个测试运行超过一定时间后,就让这次提交测试失败。我们在上一个项目中使用的这一时间是两秒。
这里还要补充一点,这个实践是一柄“双刃剑”。在创建测试时要谨防那种不强壮的测试(比如,当持续集成环境由于某种原因出现不寻常的负载时,该测试就罢工了)。我们发现,使用这种方法最好就是把它作为一种让大团队聚焦于某个具体问题的策略,而不是作为每次构建都要用到的手段。如果构建速度慢,可以用这种方法让团队暂时关注于提高速度。
编译器发出警告时,通常理由都足够充分。我们曾经用过一个比较成功的策略,即只要有编译警告,就让构建失败,但我们的开发团队常常把它叫做“纳粹代码”。这在某些场合可能有点儿苛刻,但作为强迫写好代码的一种实践,还是很有效的。
你可以通过添加代码检查尽可能地强化这一技术。我们成功使用过很多关于代码质量检查的开源工具,如下所示:
单单就流程和技术而言,分布式团队中使用持续集成与在其他环境中没有什么大的分别。但是,团队成员不能坐在同一间屋子里工作(他们甚至可能身处不同的时区),的确在某些方面会有影响。
从技术角度上看,最为简单的方法(也是从流程角度上讲最有效的方法)就是使用共享的版本控制系统和持续集成系统。如果项目中使用了后面几章将提到的部署流水线,那么共享的版本控制系统和持续集成系统应在人人平等的基础上,对团队的所有成员可用。
对在同一时区内的分布式团队来说,持续集成流程基本是一样的。
对分布在不同时区的分布式团队来说,就需要多处理一些事情啦。
对于开发大型项目的分布式团队,像Skype这样的VoIP工具和即时消息工具(IM)对于展开细粒度的沟通,顺利开展项目工作是非常重要的。
显然,这是一个比持续集成更广泛的话题。我们的主要观点是让整个流程保持一致,甚至要具有更加严格的纪律性。
一些功能更强大的持续集成服务器提供像“集中管理构建网格”和“高级授权机制”这种功能,用于把持续集成作为一个集中式服务,为大型分布式团队提供服务。这样的服务器让团队很容易建立自服务式的持续集成服务,而不需要自己管理硬件。它也会让运维团队将持续集成作为集中式服务,统筹服务器资源,管理持续集成和测试环境的配置,以确保这些环境的一致性以及与生产环境的相似性,还能巩固一些好的实践,比如第三方库的配置管理,预安装一些工具(用于收集代码覆盖率和质量的统一度量数据。最终,我们可以做到项目之间的统一度量数据的收集和监控,为管理者和交付团队提供程序级的代码质量监控方式。
虚拟化技术可以与集中式持续集成服务很好地结合,只需要单击一下按钮就能利用已保存好的基线镜像重建一个新的虚拟机。
当分布于世界各地的团队之间网络状况不佳时,依据选择的不同版本控制系统,团队间共享版本控制系统、构建和测试资源的做法有时候也会有很多麻烦。
在持续集成运转良好时,整个团队都会有规律地提交代码。这意味着,与版本控制系统之间的交互通常保持在一个较高的合理水平上。由于提交和更新比较频繁,虽然每次交互通常都较小(甚至可以用字节来计算),劣质的通信仍会严重拖生产效率的后腿。因此,加大投入在各开发中心之间建立起足够高带宽的通信机制是非常必要的。
任何一个开发中心都应该能在平等的基础上,访问那些运行有部署流水线中的版本控制系统、持续集成系统以及各种测试环境的机器。
如果由于某些不可克服的原因,无法再增加投入在开发中心间建立更高带宽的通信机制,各地团队还可以使用本地持续集成和测试系统(当然这不太理想),甚至在某些极端情况下,不得不用本地的版本控制系统。我们并不建议使用这种方法,但这种情况在现实中还是很有可能的。所以,我们要尽一切可能避免使用这种方法。这种方法在时间和人力上的成本都很高,而且根本无法做到团队间的共享访问和控制。
本书中所描述的所有技术在很多项目中已被分布式团队所验证。我们认为,在分布于不同地理位置的团队能够有效合作的重要因素中,持续集成算是仅有的两三种最重要因素之一。持续集成中的“持续”是很重要的。如果真的无从选择,与其使用一些“权宜之计”,倒不如将花一些钱在通信带宽上,从中长期来说,这是比较经济实惠的。
DVCS(Distributed Version Control System, 分布式版本控制系统)的兴起是团队合作方式的革命性改进。
DVCS使你能够离线工作、本地提交,或在将修改提交给其他人之前把这些代码搁置起来或对其做rebase操作。DVCS的核心特性是每个仓库都包括项目的完整历史,这意味着除了团队约定之外,仓库是没有权限控制功能的。所以,与集中式系统相比,DVCS引入了一个中间层:在本地工作区的修改必须先提交到本地库,然后才能推送到其他仓库,而更新本地工作区时,必须先从其他仓库中将代码更新到本地库。
这种模式挑战了持续集成的一个基本假设,这个假设就是:存在代码的单一权威版本(通常称作主干,即mainline或者trunk),所有的修改都会提交到这个主干上。可我们要说的是,使用DVCS后,你还可以使用版本控制的主干模式(mainline model)很好地做持续集成。只要你指定某个仓库作为主库(master),每次更改这个仓库就触发持续集成服务器上的一次构建,并让每个人都将其修改推送到这个仓库中来实现共享。
如果本书所介绍的开发实践里,你只想选择其中一种的话,我们建议你选择持续集成。我们一次又一次地看到该实践提高了软件开发团队的生产率。
持续集成的使用会为团队带来一种开发模式上的转变。没有持续集成的话,直到验证前,应用程序可能一直都处于无法工作的状态,而有了持续集成之后,应用程序就应该是时刻处于可工作状态的了,虽然这种自信取决于自动化测试覆盖率。持续集成创建了一个快速的反馈环,使你能尽早地发现问题,而发现问题越早,修复成本越低。
持续集成的实施还会迫使你遵循另外两个重要的实践:良好的配置管理和创建并维护一个自动化构建和测试流程。对某些团队来说,这一目标可能看起来遥不可及,但完全可以逐步达到。
持续集成需要良好的团队纪律提供支持。
总之,一个好的持续集成系统是基石,在此之上你可以构建更多的基础设施: