持续集成(By Martin Fowler)
持续集成是一种软件开发实践,在实践中项目成员频繁地进行集成,通常每个成员每天都会做集成工作,如此,每天整个项目将会有多次集成。每次集成后都会通过自动化构建(包括测试)来尽快发现其中的错误。许多团队都发现这种方法大大地减少了集成问题并且能够快速地开发出高内聚性的软件。本文简要地总结了持续集成技术及其现状。
我还清楚地记得我刚加入一个大型软件项目时的情形,那时我正在英国一个电子公司做暑期实习。我的经理(属于QA部门)领我参观了一个巨大并很压抑的房间,里面全是格子间。经理告诉我这个项目已经开发了有些年头,现在正在做集成,并且已经集成了好几个月了。经理还告诉我说,没有人真正知道完成集成工作需要多少时间。由此我学到了软件项目的一个通病:软件集成是一个漫长并且无法预测的过程。
然而,软件集成不必像这样的。在ThoughtWorks的大多数项目还有世界上许多其它组织的软件项目中,软件集成并不是什么难事。每个开发人员离共享的工程状态只有咫尺之遥,并且可以在几分钟之内将自己的代码集成进去。任何集成错误都能被快速地发现并得到快速的修正。
这种鲜明的对比并不是源自于后者有多么昂贵或复杂的工具,而关键在于每人都频繁集成这种简单实践,通常是每天向一个被管控的代码库集成。
当我向人们阐述这种实践时,通常得到两种反应:“(在我们这里)行不通”和“无关紧要”。当人们尝试了这种实践之后,他们发现其实做起来比说起来简单,而且这样的实践对于开发“至关重要”。因此有了第三种反应:“是的,我们就是这么做的,不然该怎么活啊?”
“持续集成”源自于极限编程(XP),并且是XP最初的12种实践之一。当我以咨询师的角色加入ThoughtWorks时,我鼓励我的团队使用这种技术。Matthew Foemmel将我的建议变成了实实在在的行动,由此软件集成从少有发生并且复杂的状态变成了一桩易事。Matthew和我将我们的经验写在了本文的第一版中,而本文也是我的个人网站上最受欢迎的文章之一。
虽然持续集成并不需要使用特别的工具来部署,但是我们发现拥有一台持续集成服务器将大有益处,其中最著名的有开源的CruiseControl,该软件最初由ThoughtWorks的几个员工开发,现在由一个很大的社区维护着。后来几款其它的持续集成服务器也相继出现了,有开源的,也有商业化的,包括ThoughtWorks Studios的Cruise。
在开发中使用持续集成
对于我来说,解释持续集成及其工作原理最简单的方法便是以一个小的软件功能的开发为例来进行演示。假设我们需要向软件添加一点功能,至于是什么样的功能并不重要,我们假定它很小并且可以在几个小时内完成。
首先我们需要在本地机器上保留一份当前已经处于集成状态的代码的拷贝。我通过代码管理系统在代码库的主线(mainline)上拉下(check out)一份工作代码拷贝。
上一段文字主要针对使用代码控制系统的人,对于不使用代码控制系统的人来说便是胡言乱语了。因此,我将向后者解释一下。代码控制系统用于将项目所有的代码保存在一个代码库(repository)中,项目当前的状态通常被称为主线。任何时候开发人员都可以从主线上获得一份拷贝到本地机器,这被称为“checking out”。本地机器上的代码拷贝称为“working copy”。(多数时候,实际上你是在更新(update)本地代码到主线状态,实践中它们是一样的效果。)
现在,为了完成软件的功能添加,我对本地代码进行修改,其中既包括修改产品代码,也包括添加自动化测试。持续集成非常看重测试,并且在软件代码本身中达到了测试自动化——我将其称为自测试代码,通常使用流行的XUnit测试框架的一个版本。
当我完成了功能开发(或者在我开发过程的不同阶段),我将在本地开发机上完成自动化构建。构建过程将编译并链接本地代码,然后跑自动化测试。只有当构建和测试都没有错误时,该次构建才能算是好的构建。
有了本地的成功构建,我便可以考虑将我修改的代码提交到代码库了。但是,在我提交之前,其他开发人员可能已经向主线提交了他们的修改,所以首先我需要将他们的修改更新到我本地并且重新构建。如果他人的修改与我的修改有冲突,那么在本地编译或者测试阶段将会发生错误,这种情况下,我需要负责修改本地代码直到与主线代码保持适当同步为止。
当本地代码与主线代码同步之后,我便可以向主线提交自己的修改了,代码库也得以更新。
然而,单是提交了修改并不表示我的工作就完成了。我需要再次构建,但这次是在一台拥有主线代码的集成机器上进行。只有这次构建成功了才表示我的任务完成。通常会出现这样的情况:我忘了提交本地机器上的一些东西,因此代码库并没有得到适当的更新。只有我提交的修改在集成机器上成功构建之后,我的工作才算完成。这样的集成构建可以由我手动完成,也可以由Cruise自动完成。
当两个开发者的代码有冲突时,通常会在第二个开发者更新本地代码时捕获到,否则,集成构建应该会失败。在这两种途径中,错误都可以被快速地发现。在持续集成环境中,你决不应该使失败的集成构建保留太长时间。一个好的团队每天都应该有许多成功的构建。当然,失败的构建也会时常发生,但需要尽快的修复。
这样做的结果是,我们总会得到一个稳定并且工作正常的软件。每个人都围绕着一个共享并稳定的基础代码库工作,绝不离基础代码库太远以至于需要很长的时间将自己的修改集成到基础代码库中。如此这般,我们花在找bug上的时间减少了,因为bug在频繁的集成中经常出现。
持续集成实践
上文只是关于持续集成的一个概要和它在日常开发中的工作原理。让所有这些都能很好的运作显然不止于此。现在,就让我们来看看有效持续集成所需的关键实践。
维护一个单一的代码库
软件项目需要大量的文件协同工作来构建出最终的产品。跟踪所有的文件需要大量的工作,尤其是在多个开发者参与的项目中。因此,我们可以并不惊奇的看到,不同的软件开发团队都在开发用于管理这些文件的工具——源代码管理工具,也叫配置管理,版本控制系统,代码库等。这些工具是多数软件项目不可分的组成部分。然而,令人伤心并吃惊的是,并不是所有的项目都使用了这样的工具。我的确见到(虽然很少)不使用这些工具的项目,它们使用本地和共享磁盘这种混乱的结合来共同工作。
因此,做为最基本的持续集成实践,请保证你使用一款体面的代码管理系统。成本不是问题,有许多高质量的开源代码管理工具存在。当前的选择为Subversion(译者注:现在有了更新的hg和git)。(更老的开源工具CVS如今仍然被大量使用,虽然比没有强,但是Subversion是更现代的选择。)有趣的是,当我和一些开发者聊天时,我发现相比起多数商业化的代码管理系统,他们更喜欢Subversion。据我所知,唯一值得花钱买的只有Perforce。
当你有了代码管理系统之后,确保每个开发者都能方便的获得到源代码。不应该有人还在问:“foo-whiffle文件在哪儿?”所有东西都必须在代码库里。
虽然许多团队都在使用代码库,但是我经常发现,他们并不把所有东西都放在里面。如果大家需要使用一个文件,他们知道该文件放到代码库中,但是,构建所需的所有都应该包含在代码库里,包括测试脚本,属性文件,数据库模式文件,安装脚本和第三方库等。我所知道的有项目将编译器加到代码库中的(对于早期脆弱的C++编译器来说非常重要)。基本原则是:在一台新机器上check out代码后构建也能构建成功。新机器上的东西应该尽量的少,通常包括很大的,难于安装的,并且稳定的软件,比如操作系统,Java开发环境或者数据库管理系统等。
你需要将构建所需的所有东西都加到代码管理系统中,同时也需要将大家经常操作的东西放进去,IDE配置便是一个很好的例子,这样便于大家共享IDE配置。
版本控制系统的一大功能是它允许你创建多个分支,以此来处理不同的“开发流”。这种功能很有用,但却经常被过度使用以至给开发者带来了不少麻烦。所以,你需要将分支的使用最小化,特别建议使用主线,即项目中只有单一的开发分支,并且每人在多数时间里都在“离线”工作。
总之,你应该将构建所需的所有东西都放在代码管理系统中,而不应该将构建的输出放进去。有些朋友确实将构建输出放在代码管理系统中,但我认为这是一个坏味道,可能导致更深的问题——通常是你无法完成重新构建。
使构建自动化
将源代码变成一个能运行的软件系统通常是一个复杂的过程,包括编译,文件搬移,加载数据库模式等等。但其中大多数任务都是可以自动化的,并且也应该被自动化。让人去输入奇怪的命令或点击对话框是非常耗时的,而且从根本上来说也是个错误的做法。
构建所需的自动化环境对于软件系统来说是一个通用功能。Unix的Make已经诞生好多年了,Java社区有Ant, .NET社区有Nant,现在又有了MSBuild。当你用这些工具构建和启动系统时,请确保只使用一个命令完成任务。
一个常见的错误是在自动化构建里并没有完全包括构建所需的东西。构建过程中应该从代码库里取得数据库模式文件并自动执行之。结合我上文所讲的原则来看,任何人都应该能够在一台新机器上拉下代码库中的代码,并只用一个命令将系统运行起来。
构建脚本是多种多样的,通常特定于某个平台或社区,但情况并不必须如此。我们的多数Java项目都使用Ant,而另外有些用Ruby(Ruby世界的Rake是一个非常不错的构建工具)。我们用Ant完成了早期的一个微软COM工程的构建自动化,并从中大获裨益。
大型的构建通常需要很长的时间,而在你只做了很小的修改的情况下,你是不想运行所有的构建步骤的。因此,优秀的构建工具能够分析出哪些地方需要做相应的修改,并将这个分析过程本身做为整个构建过程的一部分。通常的做法是检查源代码和目标文件的修改日期,只有当源代码的修改日期晚于其对应的目标文件时才执行编译。依赖关系因此变得微妙起来了:如果一个目标文件发生了修改,那些依赖于它的文件也需要重新构建。有些编译器能够处理这种依赖关系,而有些就不见得。
根据自己的需要,你可以选择不同的东西进行构建。构建中既可以包括测试,也可以不包括,甚至可以包括不同的测试板块。有些组件可以进行单独构建。构建脚本应该能够允许你针对不同的情形进行不同的构建目标。
我们大多数都使用IDE,而多数IDE都或多或少地集成了构建管理功能。但是这样构建文件通常是特定于IDE的,而且非常脆弱。此外,它们需要依赖于IDE才能工作。虽然对于开发者个人来说,在IDE中做这样的构建配置并无不妥,但对于持续集成服务器来说,一份能够被其它脚本调用的主构建脚本却是至关重要的。比如一个Java项目,各个开发者可以在自己的IDE中进行构建,但应该还有一个Ant主构建脚本来保证构建能在集成服务器上顺利完成。
使构建自测试
传统意义上的构建包括只编译,链接等过程。此时程序也许能运行起来,但这并不意味着系统就能正确地运行。虽然现在的静态语言已经能够捕捉到许多bug,但是漏网之鱼却更多。
一种快速并高效发现bug的方法是将自动化测试包含到构建过程中。当然,测试也不见得完美,但的确能发现很多bug——足够多了。特别是随着极限编程(XP)的升温,测试驱动开发(TDD)也使自测试代码流行起来,越来越多的人开始注意到这种技术的价值所在。
经常读我著作的读者可能知道我是一个TDD和XP的大粉丝,然而我想强调的是这两种方法和自测试并没有必然联系。TDD和XP都要求先写测试代码,再写功能代码使测试通过。在这种模式下,测试既用于发现bug,又用于完成系统设计。这是非常好的,但对于持续集成来说不必如此,因为此时我们自测试代码的要求并不那么高。(然而TDD是我写自测试代码的首选。)
对于自测试代码而言,你需要一组自动化测试来检测一大部分代码库中的bug。测试能通过一个简单得命令来运行并且具备自检功能。测试的结果应该能指出哪些测试是失败的。对于自测试的构建来说,测试失败应导致构建失败。
过去这些年里,TDD使开源的XUnit家族流行起来,成为了理想的测试工具。在ThoughtWorks,XUnit已经是非常有用的测试工具,我也经常建议人们使用。这组工具起初由Kent Beck开发,它们使自测试环境的搭建变得非常简单。
XUnit当之无愧地是你进行代码自测试的起点。当然,你也应当多看看那些更侧向于端到端测试的工具,包括FIT,Selenium,Sahi,Watir,FITnesse等等,我就不逐一列举了。
当然,别指望测试就是万能的。常言道,测试并不代表就没有bug。
每人每天都向主线提交代码
集成首先在于交流,它使其他成员能够看到你所做的修改。在这种频繁的交流下,大家都能很快地知道开发过程中所做的修改。
在向主线提交代码之前,开发人员必须保证本地构建成功。这当然也包括使测试全部通过。另外,在提交之前需要更新本地代码以匹配主线代码,然后在本地解决主线代码与本地代码之间的冲突,再在本地进行构建。如果构建成功,便可以向主线提交代码了。
在这种频繁提交下,开发者可以快速地发现自己代码与他人代码之间的冲突。快速解决问题的关键在于快速地发现问题。几个小时的提交间隔使得代码冲突也可以在几个小时内发现,此时大家的修改都不多,冲突也不大,因此解决冲突也很简单。对于好几周都发现不了的冲突,通常是很难解决的。
在更新本地代码库时就进行构建,这意味着我们既可以发现文本上的冲突,又可以发现编译冲突。既然构建是自测试的,那么运行时的冲突也可以被检测出来,而这样的冲突往往是一些特别烦人的bug。由于提交间隔只有短短的几个小时,bug便没多少藏身之处了。再者,因为每次提交的修改都不多,你可以使用diff-debugging来帮你找出这些bug。
我的基本原则是:每个开发者每天都应当向代码库进行提交。在实践中,越是频繁提交,可能导致冲突的地方就越少,因而也越容易发现。
频繁提交鼓励开发人员以几个小时为单位来分割他们的代码,这样便于跟踪进度。通常,人们一开始认为在短短的几个小时内做不了什么事情,但我们发现找个导师和多实践可以帮助他们学习。
每次提交都应在集成机上进行构建
有了每日提交,也就又了每日测试,这应该表明主线处于健康状态。但是在实践中,的确有出错的时候,原因之一在于纪律——有人并没有在提交之前进行本地更新和构建。另外,不同开发机之间的环境不同也是一个原因。
因此,你应该保证在集成机上进行构建,只有当集成机上构建成功后,才表明你的任务完成了。由于提交者需要对自己的提交负责,他就得盯着主线上的构建,如果失败,马上修改。如果下班之前你提交的修改失败了,那么,对不起,请修改好了才回家。
我见到过两种方式来保证主线构建的成功:一是手动构建,二是使用持续集成服务器。
手动构建是最简单的,基本上与开发者在本地做的构建差不多——先到集成机上拉下主线的最新代码,然后运行构建命令,在构建过程中你得盯着构建过程,如果构建成功,表明你的任务完成。(另见Jim Shore的描述。)
持续集成服务器则一直监视着代码库,一旦检测到有提交,便自动拉下代码到本机,然后开始构建,并将结果通知提交者。只有当提交者收到通知后——通常是以电子邮件的方式,才表明自己的任务完成。
在ThoughtWorks,我们是持续集成服务器的忠实粉丝,我们领导了CruiseControl和CruiseControl.NET的初期开发,此两者均是广为使用的CI服务器。从那时起,我们也开发了商业化的Cruise。在几乎每个项目中,我们都使用了CI服务器,并且结果是令人愉悦的。
不是所有人都倾向于使用CI服务器的,Jim Shore便给出了一个很好的论述,在此论述中,他解释了为什么他更倾向于手动构建。我同意他的看法——CI不过是安装一些软件而已,所有的实践都应当旨在有效地完成持续集成。但同样,许多使用CI服务器的团队的确发现CI服务器是很好的工具。
有很多团队定期的进行构建,比如每晚构建。这和持续构建并不是一回事,而且对于持续集成来说,也是不够的。持续集成的关键在于尽快地发现问题。而每晚构建意味着整个白天都发现不了bug,如此,需要很长的时间发现并清楚这些bug。
持续构建的重点在于,如果主线构建失败,你应该马上进行修改。在持续集成中,你一直是在一个稳定的代码库基础上进行开发。主线构建失败并不是一件坏事,但是,如果这样的情况经常发生,那么就意味着开发人员对于本地更新并没在意或者在提交之前并没在本地构建。主线构建一旦失败,必须马上修正。为了避免主线构建失败,也许你可以试试pending head。
快速构建
持续集成的关键在于快速反馈,需要长时间构建的CI是极其糟糕的。我的多数同事都认为一个小时的构建时间对于CI来说决无道理可言。我也记得曾经有团队梦想着他们的构建能有多么多么的快,但有时我们不得不面对很难快速构建的情况。
对于多数项目来说,将构建时间维持在10钟之内是合理的,这也是XP的方针之一,我们多数项目也达到了这个目标。这种做法是值得的,因为这样省下的时间是为开发者节约的。
如果你的构建长到了一小时,那么想使其加速便不是那么容易了。对于企业级应用来说,我们发现构建时间的瓶颈通常发生在测试上,特别是那些需要于外部交互的测试——比如数据库。
可能最好的解决办法是引入阶段性构建(也叫构建管道或者部署管道),因为构建事实上是分阶段性的。代码提交后首先触发的是构建称为提交构建,提交构建应该快速完成,而棘手的是怎么保持速度与查找bug之间的平衡。
提交构建成功后,其他人便可自信的工作了。但是,你可能还有其它跑得比较慢的测试需要写,这时可以用额外的机器来专门跑这些耗时的测试。
一个简单的例子是将构建分为两个阶段,第一个阶段完成编译,并且跑那些不需要外部交互的单元测试,数据库交互也通过stub的方式完全消除掉。这些测试可以很快跑完,原则是将其保持在10分钟之内。但是,对于那些需要大量外部交互——特别是涉及到真实数据库交互时才能发现的bug,这个阶段便无能为力了。第二个阶段跑的测试则需要操作真实的数据库了,同时还应包括端到端测试。这个阶段可能需要几个小时。
在这种情况下,通常将第一阶段视为提交构建,并将此做为主要的CI周期。第二阶段则可在有必要时才进行,如果这个阶段构建失败,它也不需要像第一阶段那样“停下全部手头的工作”,但也应该得到尽快的修改。第二阶段的构建不见得需要保持一直通过,对于已经发现的bug来说,可以在之后几天修改。对于这个案例来说,第二阶段全是测试,因为通常情况下最慢的即是测试。
如果第二阶段构建发现了bug,通常意味着应该在第一阶段中引入新的测试来予以保证。
当然,以上的两阶段构建只是一个例子,你完全可以加入多个构建阶段。提交构建之后的其它构建是可以并行完成的,如果这些阶段的构建需要好几个小时,那么可以用几台机器来并行完成。通过这种并行化,你可以将提交构建之外的所有测试都引入到构建过程中来,比如性能测试。
在与生产环境的拷贝环境中运行测试
测试旨在发现可能在生产环境中出现的问题,因此如果你的测试环境与生产环境不同,那么测试很有可能发现不了生产环境中的bug。
因此,你的测试环境应该尽量与生成环境相同。使用相同的数据库,相同的操作系统,并且版本都应该一样。另外,将生产环境中的库文件也放到测试环境中,即使构建时用不到这些库。IP地址和端口号也应当相同,当然还包括硬件。
但事实上这是有限制的。如果你开发的是桌面软件,很难预测你的客户在使用哪些第三方库。再者,生产环境可能非常昂贵。即便存在这么多限制,你依然应当尽量去复制生产环境,并熟知因测试环境和生产环境的不同而可能导致的风险。
如果你搭建的环境足够简单并没有多少烦人的外部交互,那么你的提交构建便可在仿真环境中进行。但是,由于系统反应慢等原因,你可能需要test doubles。因此,通常情况是在人工环境下跑提交构建以获取速度,而用一个生产环境的拷贝环境来跑其它测试。
我注意到,虚拟化技术越来越引起人们的兴趣。由于虚拟机可以保存构建所需的所有东西,故在虚拟机中运行构建和测试相对比较容易。另外,虚拟机技术也允许你在一台机器上运行多个测试,或者可以模拟多台机器同时访问网络的情况。随着虚拟机性能逐渐提升,它将引起更多的注意。
使任何人都能轻易获得可执行文件
软件开发最困能的事情之一便是你不能保证所开发的是正确的软件。我们发现人们往往很难预知自己究竟想要什么,而相反,对已有的东西进行评判和修改却容易的多。敏捷开发过程则恰恰是符合人类这种行为习惯的。
为此,项目中的所有成员都应能够获得最新的可执行文件并能成功的运行,目的可以包括做演示,浏览测试或者仅仅看看项目本周有何修改。
这是很容易达到的:确保一个通用的地方来存放最新可执行文件。在同一个地方存放多个可执行文件也是很有用的。对于最新的可执行文件,应当保证能够通过提交测试。
如果你的开发过程有一个很好的迭代计划,将每次迭代最后一次构建生成的可执行文件存放起来也是明智的做法。
人人都能看到正在发生什么
持续集成主要在于交流,因此应当保证每人都能轻易看到当前系统的状态和已做的修改。
主线的构建状态是非常重要的,Cruise服务器包含一个网站,你可以在该网站上看到当前的构建状态和最后一次主线构建的结果,许多团队喜欢用比较显眼的标识来反应构建状态,比如在屏幕上放一盏灯,灯绿表示构建成功,灯红表示失败。尤其常见的是lava lamps——不仅表明构建状态,还可显示构建时间。如果红灯中有了气泡,则表明构建已经失败了很长一段时间了。每个团队都有自己的选择,当然,适合自己的才是最好的。
对于手工完成的持续集成过程,这种可见性也是很重要的,构建机器的显示器应该能显示主线构建的状态。通常,正在做集成的人会放一个token在桌上来表明他正在做集成。人们喜欢在构建成功后播放一些简单的声音,比如闹铃之类的。
当然,CI服务器的网站可以展示更多的信息。Cruise不但能可以显示是谁在构建,并且能显示最新提交的修改。另外,Cruise还可以查看提交历史,这样,团队成员便可以很清楚项目的进展情况。据我所知,有些团队的头便是通过这种方式来了解项目成员的工作情况和整个系统的修改情况。
使用CI网站的另一个好处是,哪怕不在一起工作的人都可以看到当前项目的状态。再者,你也可以将不同项目的构建信息放到一起。
并不是只有CI网站才能展示显示构建信息。由于构建的不稳定性是一直存在的,这时我们可以将全年的日历画在一张墙上,每天对应一个方块,如果构建成功,QA则在该天的方块贴上绿色标签,否则贴上红色标签。时间一久,这份日历便可显示出项目的稳定性进展情况。
自动化部署
做持续集成需要多种环境,不同的构建阶段需要不同的环境。每天,项目的可执行文件都会在这些环境之间搬来移去,于是你希望将这些过程自动化。因此,自动化部署脚本便很重要了,不仅包括测试环境的脚本,也包括针对生产环境的部署脚本。虽然我们不是每天都向生产环境部署,但自动化部署不仅可以加速部署过程,并且能够减少部署错误。
如果你已经有了生产环境的自动化部署,那么也应该考虑一下相应的自动化回滚。由于失败是时而会发生的事情,在这种情况下,我们希望能快速回滚到失败之前的状态。这样一来,我们在部署是也不用那么畏首畏尾了,于是我们可以频繁的发布软件,用户亦能尽快的享受到新的功能。(Ruby on Rails社区有一款名为Capistrano的工具即是一个典型的例子。)
在集群环境中,我看到有每次只向一个节点部署的情况,由此在几个小时之内逐渐完成所有节点的部署。
对于一些面向大众的web应用,我所了解的另外一种很有趣的部署方式是,先试验性针对一部分用户进行部署,再通过这些用户的试用情况来决定是否向所有用户部署。自动化部署,做为CI的一项原则,能够很好的胜任这些工作。
持续集成的好处
总的来说,我认为持续集成的最大好处在于降低风险。我又想起了我在本文一开始提到的那个项目——已经处于项目的末期,但是仍然不知到何时才能结束。
延期集成的缺点在于,很难预测集成到底要花多少时间,更糟的是,你很难了解集成的进展情况。
持续集成正好解决了这些问题。每次集成的时间都不长,任何时候你都知道自己所处的情况,软件的哪些地方在工作,哪些没有。
Bug——恶心的玩意儿,伤害我们的自信,搅乱我们的日程,还破坏我们的名声。如果在生产环境中遇到了bug,那么用户将会把气往你身上撒。而在开发环境中,bug拦着你的路,迫使你无法完成余下的工作。
持续集成并不能消除bug,却能帮你快速的发现bug并予以清除。这种情况下,持续集成更像是自测试的代码。当遇到bug时,由于你只做了很小的修改,这样便大大缩小的bug的查找范围。另外,由于是你刚写的代码,你还记得很清楚,因此也使查找bug更加容易。你还可以使用diff debugging,将当前的代码版本和先前没有bug的版本进行比较。
Bug也存在积累性,bug越多,越难清除。部分原因在于bug之间存在牵连。另外也存在心理因素,bug一多,人便没那么多精力去修了——这就是所谓的“破窗效应”。
因此,对于采用持续集成的团队,bug将大大减少,不管是在生产环境,还是在开发环境。但是,我想强调的是,你的获益程度取决于测试的好坏程度。你或许已发现,写出好多测试并不难。然而,要达到低bug率的程度依然是需要时间的,你还得不断地引入并改进自己的测试。
有了持续集成,频繁部署也不是什么难事了。频繁部署的价值在于,你的客户可以快速的享用软件的新功能,并能快速的提出反馈。这将有利于清除客户和开发之间的障碍——我认为这是软件开发最大的障碍。
引入持续集成
然后你开始试着玩持续集成了,但该从何入手呢?上文中我所罗列持续集成实践可以给你带来太多的好处,但是你并不必在一开始就完全采用这些实践的。
做持续集成没有套路,主要取决于你团队自身的情况,但是我们发现以下几点对于持续集成来说是比较通用的。
-
(1)第一步需要将构建自动化,并将你所需的所有东西都放在代码管理系统中,以至于可以通过一个命令来构建整个系统。对很多项目来说,这并非易事。一开始,你可以按照需要进行构建,或者可以只做自动化的夜晚构建。虽然,这些做法都不能称为持续集成,但夜晚构建确是一个好的起点。
-
(2)在构建中引入一些自动化测试,试着确定出现问题的主要范围,并用自动化测试去发现这些问题。对于已有的项目来说,很难建立起一组好的快速测试,这时你就得另寻它路了。
-
(3)使提交构建快速完成。虽然好几个小时的持续集成比没有要好,但是如果你能将构建时间缩短到几十分钟,或者就短短的10分钟,这就再好不过了。
-
(4)对于新项目,从项目开始就采用持续集成。注意构建时间,如果构建时间违背了“10分钟原则”,那么请尽快采取行动。
-
(5)寻找帮助,找有经验的人帮助你。和其它的新技术一样,当不知到结果会是什么样时,很难开头。找一个导师可能要花钱,但是不找的话,你所付出的代价是时间的浪费和低下的生产力。(ThoughtWorks提供这样的咨询服务,毕竟你可能遇到的问题我们之前都遇到过。)
总结
自Matt和我发布了本文的第一版之后,持续集成逐渐变成了软件开发的主流技术,在ThoughtWorks,几乎所有的项目都使用到持续集成,同时我们也看到世界上其他组织也在使用持续集成技术。相比起充满争议的极限编程来说,持续集成很少得到负面的评论。
如果你还没有采用持续集成,我强烈建议你试一试。如果你已经采用了持续集成,本文可能会帮助你进一步提高效率。这些年来,我们已经学到了许多关于持续集成的知识,我们也希望有更多可以学习和改进的地方。