3年前,我在一个由20余人组成的产品团队开始了软件开发工作,随着项目规模的膨胀,某些bug重复出现,
延迟发布,团队熬夜工作,开发团队和测试团队关系紧张。难道软件开发只能如此?带着困惑,我加入了ThoughtWorks。在这里,进行开发是一件高效而有趣的事情。那么,是什么造成了两者间差别:
X公司
ThoughtWorks
单元测试 JUnit
JUnit
/ JSUnit
功能测试 无
JWebUnit
集成测试 人工
Selenium
验收测试 人工
Selenium
性能测试 Jmeter
Jmeter
集成 人工
CruiseControl
ThoughtWorks 的开发过程采取了更多自动化的开发方式。稳定而高效的开发效率保证了开发团队在一个轻松愉快的环境中工作,同时团队成员可以有更多的时间和精力学习新技术并将其应用在软件开发中。生产力的发展过程是不断采用物化劳动取代人自身的劳动的过程,是不断自动化的过程。自动化测试/集成将我们从简单,繁琐的低级脑力劳动中解放出来,从而进行更高层次的思考。这篇文章主要探讨如何在开发过程中使用自动化持续集成、使用持续集成工具的优势、以及如何通过持续集成工具管理产品的整个生命周期。
什么是持续集成?
持续集成一种软件开发实践。通过它,开发团队的成员频繁的整合他们之间的工作。它不是简单的组装软件而是软件开发过程的核心实践,通过时时运行测试,保证软件现有的功能不被破坏,自动分析现有代码的状态(有无重复逻辑,代码的复杂度等)并发布相关的报告。这些功能根据开发团队所采用的持续集成服务器不同而有所不同,如TeamCity
采用自动的服务器端分析,而开源项目CruiseControl要求开发团队采用Checkstyle, Emma,
Simian 等代码分析工具。
持续集成的价值?
- 减小风险
持续集成过程通常在开发人员提交代码后开始,服务器自动更新代码,编译,运行单元测试,功能测试,集成测试,进行部署。这个持续集成的过程可以帮助开发人员快速发现并解决问题(编译失败,测试失败等)。与开发人员的机器相比,持续集成服务器运行在相对稳定、干净的环境中(减小跟踪调试的难度),持续集成过程的失败通常意味着最近一次更新破坏了软件现有功能或引入了新的缺陷。在持续集成过程结束后,除了构建结果(War,
Jar等),通常会生成代码分析报告(测试覆盖率等),帮助项目管理人员更好的了解并改善项目。
- 减少手动过程
在开发过程中大量的采取手动过程不仅降低了团队的生产率,更严重的是它将许多不确定的因素引入到产品的构建过程中,这使得发现以及解决问题变得异常困难。
QA在测试环境中所发现的问题可能是由于部署人员忘记拷贝某个配置文件,也可能是由于没有删除某些临时文件引起的。通过使用持续集成工具将构建过程自动化,便于分析并找出问题,大大提高了团队的效率。
- 生成构建结果
从客户和用户的角度看,一个可以部署或者执行的构建产物才是最重要的。在使用持续集成工具的环境中,开发人员对源文件进行修改,提交,持续集成服务器会将这部分修改与其他的代码进行整合,测试,并重新生成最终产品(War,
Jar, exe等)。如果在其中任何一个环节出现了问题,相关人员可以很快的得到反馈。在没有使用持续集成工具的环境中,大量的问题只有在产品发布前进行最终集成的时候才会出现,开发团队往往在发布前承受着巨大的压力,并导致产品延迟发布或者在进行hot
fix的过程中引入更多、更严重的缺陷。
- 安全感
软件开发过程最终表现为人与人之间各种形式的合作。安全感与信心是合作最基础也是最重要的部分。通过使用持续集成工具,开发人员可以了解到新的代码是否引入了缺陷,管理人员可以通过使用各种形式的报告对项目进行评估。不断发布的构建结果,使测试人员得以自始至终的参与到整个开发过程中,而不是在软件开发的最后阶段才加入团队。
如何进行持续集成?
在进行持续集成实践前,应当正确的选择并配置持续集成服务器。列出了所有的持续集成服务器 ,其中比较成熟的持续集成服务器包括:CruiseControl,
Anthill, Bamboo, TeamCity, Continuum 等。CruiseControl作为开源产品,以其对于各种SCM以及构建工具的广泛支持而被许多开发团队所接受。
在一个典型的持续集成过程中:
- 开发者每次将代码提交到SVN之前,必须运行本地测试: 尽量保证不会破坏持续集成服务器的构建过程。
- 开发者每天进行多次提交:小步前进会大大减少服务器构建失败的概率,并且使得修复失败构建的时间大大缩短。
- 持续集成在一台服务器上不断运行:保证在稳定的环境中进行测试。
- 所有的测试必须全部通过:保证软件现有的功能不被破坏并且没有引入新的缺陷
- 生成构建结果(War, Jar,exe等) : 用以开展下一步的工作,譬如QA的探索测试等。
- 生成报表 :帮助管理人员评估开发状态。
- 修复失败的构建 : 失败的构建意味着软件现有的功能已经被破坏或者有新的缺陷被引入,修复的速度越慢使修复难度越高,并带来更大的损失。
持续集成坏味道:
- 持续编译
[现象]某些团队仅仅使用持续集成服务进行编译并生成最终的构建结果。
[影响]持续集成无法给开发人员,管理人员带来有价值的快速反馈。
[原因]开发团队可能缺乏编写易于测试的代码的能力,或者不了解现代软件开发中测试的流程和作用
[解决方案]测试优先,单元测试,功能测试,验收测试
- 构建长时间失败
[现象] 没有开发者愿意修复失败的构建,持续集成工具上的构建已经持续失败很长时间。
[影响] 开发者忽视持续集成服务器发布的结果,修复构建的成本和难度升高,开发团队,管理团队无法得到快速反馈,丧失安全感
[原因] 长时间不进行代码更新并一次提交太多代码,构建时间太长导致开发者缺乏耐心运行本地构建、任务过于复杂
[解决方案] 简单设计,小步前进,缩短构建时间
- 过多失败构建
[现象]持续集成服务器上有很多失败的构建、开发者常常在持续集成服务器上强制运行构建
[影响]团队其它成员无法提交代码,开发效率下降。
[原因] 通常这是项目中存在随机失败测试的信号,譬如,某些测试存在顺序依赖,时间敏感或者没有在测试结束时正确回收资源。这样,虽然开发者本地构建通过,却无法保证在持续集成服务器上成功构建,开发者会不断的尝试在服务器端重新运行构建试图得到一个成功的构建
[解决方案] 简单设计,编写正确的单元测试
- 构建时间过长
[现象]本地构建时间超过10分钟
[影响]生产率严重下降
[原因] 可能是由于重复测试引起,由于测试之间没有很好的隔离,导致同一逻辑在对不同对象进行测试时被重复测试、或者是软件规模大,测试多引起
[解决方案] 分布式构建
- 构建结果不醒目
[现象] 没有开发者意识到持续集成服务器上的构建已经失败了
[影响] 构建长时间失败,修复难度变大
[原因] 没有将构建结果明显的发布出来
[解决方案] 安装构建指示灯,或者在构建失败的时候播放音乐。
拥抱持续集成:
为了享受持续集成带来的诸多好处,开发者需要做到:
- 小步前进,频繁提交
- 不要提交本地测试失败的代码
- 编写可以自动进行的测试
- 编写可以快速运行的测试
- 如果构建失败,第一时间进行修复
- 如果构建失败,拒绝更新代码
不进行持续集成的理由:
- 硬件花费
对于大多数的团队来说,持续集成服务器不需要运行在一台性能异常强大的机器上,使用一台开发机器加一个指示灯(在构建失败时变红)已经足够了,硬件的成本在不断的下降,和开发团队不断修复回归测试发现的缺陷,编译失败,延迟发布比起来,这个投资应该是非常划算的。
- 管理开销
目前的持续集成服务器已经比较成熟,并且有许多免费的产品(如CruiseControl)可以选择。这些服务器不论是运行还是配置都非常简单。开发团队可以根据熟悉的编程语言选择相关产品,以CruiseControl为例,分别有CruiseControl.rb
(Ruby), CruiseControl.net (.Net), CruiseControl(Java)可以进行选择。这样开发团队可以在没有系统管理员的清况下,自行维护持续集成服务器。
- 减慢了开发者提交的速度
开发者花10分钟进行测试,节省了整个团队修复构建的时间,免去了不必要的交流成本,节省了修复bug的成本,
大大减少了延迟发布的可能。
- 学习自动化测试
不要重复你自己! 花半天学习Ant, Maven, Shell脚本比每天花半个小时手工测试要有趣得多,引入自动化测试,节省的不仅仅是个人的时间,整个团队效率也因此提高。
让持续集成过程更强大:
- 流水线构建
作为持续集成实践的一部分,在完成代码编译后,必须将软件部署到测试或者产品环境中,在现代软件构建过程中,部署不再仅仅是将某个2进制文件拷贝到文件系统中,它通常包括了部署以及对于应用本身、数据库、Web容器等进行配置的过程。在进入产品环境前,软件通常会经过开发以及测试人员在开发环境,QA环境,性能测试环境,模拟产品环境的重重测试。由于环境以及操作的复杂,大量的手工劳动譬如修改配置文件在这个时候被引入进来。
流水线构建的出现就是为了解决上述问题:一个RC版本需要通过所有的测试阶段,在每一个阶段中,产品都会被测试并且进行某种形式的修改来准备下个阶段的测试。产品在管道中不断的前进,并且被不断检验,组装,修改。如果产品能够成功的到管道的终点,它就可以被认为是一个成功的RC版本。
流水线构建被划分为提交阶段, 验收阶段, 性能测试阶段/手工测试阶段, 产品部署等4个阶段。
提交阶段:是管道的第一个部分。当开发者提交代码后,产品就进入这个阶段。在提交阶段,代码被编译,相关的单元测试被运行。这个阶段所需要的测试时间必须尽量短,开发者需要等到这个阶段成功完成才可以开始下一个任务。这个阶段不是通过测试类型教条划分的,我们应该根据哪些测试可以被快速运行并且帮我们发现大多数常见的问题来划分测试。譬如,在我现在参与的项目中存在JUnit,JWebUnit
, JSUnit, Selenium 测试等,Selenium 花费的时间相对长,而且运行JUnit,
JWebUnit以及JSUnit在通常清况下,已经可以帮助我们发现大多数的问题,那么它们将成为我们能否能通过提交阶段的依据。
验收阶段: 在很多清况下,我们的软件可以通过单元测试,功能测试,但是这并不意味着有了一个成功、可用的软件,因为它也许与我们的业务需求背道而驰,验收测试用于验证软件是否更够满足业务需求,体现商业价值。验收测试通常运行在开发者本地测试相似的环境中(使用相同的OS,
JDK等)。只有能够通过验收阶段的产品才有可能成为RC版本的软件. 在这个阶段,我们应当使用提交阶段的构建结果。而不是重新编译,打包。(想象一下,软件正在这个管道中移动,我们要做的是继续组装而不是拆卸),例如,提交阶段结束后,一个war被拷贝至文件系统某个特定的目录,而验收测试服务器通过扫描目录发现更新并通过使用它进行Selenium
测试。
在进入下一个阶段前,我们需要通过部署阶段。在这个阶段,我们需要初始化测试环境,并根据测试环境的需求,修改配置文件。初始化测试环境对于整个构建管道非常重要,它确保了产品在确定的环境中运行,减小分析问题的难度。这正是工业自动化的精髓所在:尽量的在生产过程中,通过自动化去除不确定因素。初始化测试环境通常是通过还原镜像文件来进行,我们根据目标测试环境制作不同的镜像文件,并且在部署还原镜像文件。这个阶段可能是自动的也可能是手动的。当然,原则是尽量自动化进行,但是如果难度过大,手工还原也可作为一种妥协。
性能测试和手工测试:它们可以同时在不同的服务器上进行。自动化测试只能阻止已知缺陷不会再出现。但是无法验证测试用例之外的情况。手工测试或者叫做探索测试
正是在这种情况下进一步的测试软件的行为。
产品部署 :整个管道的最终阶段,验证软件是否能够真正的安装并运行在目标产品环境中。
- 分布式构建
某些持续集成工具(如CruiseControl)支持合成构建模式,即当且仅当所有子构建全部成功,主构建才会成功。分布式构建(以CruiseControl为例)就是建立在JINI以及合成构建的基础上。通过使用分布式构建,我们可以大大缩短构建时间以及在不同的环境下对产品进行构建。
分布式构建通过将主构建分为若干可以同时进行构建的模块来加快速度,譬如、某项目可能由:登录模块测试(10分钟),付款模块测试(20分钟),管理模块测试(20分钟)。在这种清况下,我们可以部署4个CruiseControl实例(一个作为主构建,其余三个作为子构建分别重复安装目标项目)。子构建分别运行不同模块的测试,而主构建负责收集根据子构建成功与否决定整个构建是否通过。与上述的实践相似,通过将实例部署在不同环境中(Windows,
Linux, Unix等),我们也可以验证产品是否可以在不同环境中进行构建。
关于分布式构建,ThoughtWorks发布了一份详细的文档指导开发者如何一步步进行配置,在此不再赘述。
总结:
持续集成是软件开发一个重要的实践,通过持续集成,我们能够更早的发现问题,更快的解决问题,同时大大减少了构建过程中不确定性,提高团队的生产效率。当然,持续集成实践不是孤立的,它与其他的软件开发实践例如测试驱动等紧密相关。正确使用持续集成实践将帮助我们在一个轻松的环境中快速开发,发布高质量的产品。
参考:
The
Deployment Pipeline
Continous Integration
CruiseControl Reference
Using distrib from the CruiseControl contrib |