持续集成(三):最佳实践


【编者的话】这是持续集成系列的最后一篇,在本文中,作者列出了Martin Fowler撰写的CI白皮书里面的一些原则,并介绍了一些个人的实践经验。

引言

这是持续集成系列的第三篇。在这篇文章里,我们将介绍实现一个CI流程的一些最佳实践。笔者也将会根据自己的行业经验介绍一些真实世界里的提醒和警告。

快速回放:在本系列的第一篇里,我们介绍了CI的基本概念以及它和敏捷开发及DevOps团队文化的关联。在第二篇里,我们介绍了CI服务器的概念以及它是如何将各种实现一个CI流程的行业标准实践无缝整合到一起。

如果你还没有读过前面的文章的话,笔者强烈建议在继续阅读本文之前先翻阅一下!

Martin Fowler,在他的CI白皮书里提到了一些应当成为任何CI设定一部分的关键实践。这些建议多年来已然成为"这样"一组持续集成的最佳实践。同一主题的维基百科页面则对外展示了Martin Fowler所阐述的那些原则的本质。

下面,笔者将以自己个人的视角和大家一起来看看这些最佳实践里的每一项。

最佳实践

维护一个单一的源码仓库

"这种做法主张对项目的源代码使用一个修订版控制系统。所有需要用来构建该项目的素材都应该放到仓库里。按照惯例,采取这样的做法并且是在一个修订版本控制社区里,该系统应该是可以基于一个全新的签出做构建,而无需任何额外的依赖。极限编程倡导者Martin Fowler还提到,在工具支持分支的情况下,对它的使用应该最小化。相反,我们推荐的是将变更集成进来而不是同时维护软件的多个版本。主线(或者主干)应该是软件可工作版本代码的存放位置。"

提醒

  1. 该原则不能单从字面上去理解,这并不意味着你只需要一个单个的仓库。这里的关键是所有构建项目所需的素材都可以在一个仓库里找到。该项目应当可以基于一个全新的签出构建,并且不需要额外的依赖或者手动步骤。

  2. 这里'项目'的定义取决于你,如果代码仓库很小的话它可能意味着整个可交付的产品,或者可以是组织好的代码里任意逻辑模块或者组件。


警告

该原则原本建议不要在版本控制系统里使用分支。相反,它建议项目由始至终仅在一个单一分支下开发。

不过,笔者并不赞同这一点。在绝大多数组织里,在多个分支下并行开发是很有必要的。企业往往需要支持产品之前发布的版本,修复其中的错误,而其他的团队成员则开始下一个版本的工作。这就需要在代码库里维护多个分支。

构建自动化

"构建系统应当一条命令就能办到。许多构建工具,比如make,已经存在很多年了。其他更多近期涌现的工具经常用在持续集成的环境里。构建的自动化应该包含自动集成,这通常包括部署到一个类生产环境里。在许多情况下,构建脚本不仅可以编译二进制文件,还可以生成文档,网站页面,统计信息和发行版媒介(如Debian DEB,Red Hat RPM或Windows MSI文件)。"

提醒

  • 构建的自动化应当包括诸如编译代码,执行单元测试以及集成测试等步骤。 它们也许还包括许多其他工具 - 如前面文章里描述的代码质量检查,语义检查,衡量技术债等。绝大多数现代构建工具都支持这些额外的集成,而且应该可以用于建设持续集成环境。

  • 在现实世界的项目里,不同的团队可能负责开发系统的不同部分,每个团队都拥有自己的仓库。 在这种情况下,几乎不可能(没有重大工作的话)而且也完全没必要基于整个产品做自动化构建。 一般来说,为系统的每个单独部分开发自动构建就足够了。


警告

  • 定义CI流程的目的,即除了自动化构建流程外,是否还有其他的投入点?作为CI流程的一部分,你计划测量哪些指标。很多时候,笔者见到的是CI设定被视为单独只是开发人员的工具。

  • 延伸之前一点的话,CI不是敏捷开发/DevOps,它们只是针对整个组织成功实施CI流程所使用的工具之一。敏捷开发/DevOps的范式可以超越软件开发的技术层面,并扩展到组织的文化里。


让构建自检

“一旦代码被构建,所有测试都应该被执行以确认它的行为如开发人员们预期的那样。”

提醒

  • 代码应当至少包含单元测试。像JUnit这样的框架可用于轻松地模拟依赖。

  • 特定组件与其他模块的交互应当被模拟取代。 这可以确保一个模块能够独立于其他模块进行测试。


警告

  • 单元测试应该测试行为,而不是实现细节。有什么区别呢?我们不妨通过一个例子来说明:测试行为的话:"我不关心你怎么计算汽车的速度,保证答案是对的就行",如果是测试实现细节的话:"我不关心答案是什么,只要确保你采用的是这个公式:速度=距离/时间即可",测试行为是正确的方法,因为我们只需要验证结果就行了,而不是方案是如何实现的。

  • 许多测试框架允许我们声明模拟对象,即测试模拟对象是否被调用,是否在特定参数中被传递。这些资源应当被最小化,除非实现本身的测试是主要的关注点。


人人每天都提交到基线

“通过定期提交,每位提交者都能借此减少冲突变更的次数。一周工作产出在签入时与其他功能冲突的风险可能很难解决。在更早期的阶段,系统某块领域的小冲突会促使团队成员就其所做的改变进行沟通。至少每天提交一次更改(每次创建一个功能)通常被认为是持续集成定义的一部分。此外,一般建议每晚进行一次构建。这些是下限;业内持续集成的典型频率预计会高得多。”

提醒
  • 代码应至少包含单元测试。像JUnit这样的框架可用于轻松地模拟依赖。
  • 特定组件与其他模块的交互应当被模拟取代。


(原文这一部分的Tips可能存在谬误,译者注)

警告
  • 已经完成的工作应当提交到主分支。主分支应当总是可工作版本的软件代码。
  • 如果看到哪次构建失败的话请不要提交分支。你应该先验证下是什么导致的错误,然后尝试尽快解决而不是提交自己的代码。为什么在构建失败的时候不应该签入你自己的代码呢?首先,你自己的提交可能存在一些问题,它可能会破坏一些预期的行为。你不会知道这些问题是什么,除非得知上一次签入时构建的状态。而且每一次签入都有可能因为添加了现有的错误让问题变得更糟。


应当构建每一次提交(到基线的)

“系统应当构建每一个合并到当前工作版本的提交,从而验证它们集成地很好。常见的做法是利用自动持续集成,尽管这可以手动完成。对大多数情况而言,持续集成是采用自动持续集成的同义词,一台持续集成服务器或者守护进程会监控校订版本控制系统的变更,随后自动运行构建流程。”

提醒
  • 用户应当分离主分支和其他分支的CI工作流。这些步骤包括从编译到打包再到测试。主分支的构建一般应当包含更多的测试。主分支的构建也可能需要运行不同的脚本,因为应用可能需要针对不同的部署平台打包成不同的格式。在其他分支上运行的构建可能根本不需要打包这一步,或者通常局限于与开发人员相同的平台的打包。
  • “夜间构建”也应当在每晚计划好的时间点执行。相比于其他分支而言,该构建应当包含更多的验证过程。它需要花费更长时间去运行并且执行频度更低。


警告
  • 主线分支里不应该注释测试。将测试注释掉的话,我们得到的会是构建状态的错误提示。
  • 引入编码标准的检查是CI流程的一部分。代码必须经过自动化工具以及团队成员检查,然后才能签入到主线。


保持构建速度

“构建需要快速完成,这样一来如果存在集成问题便会立马被识别出来。”

提醒

  • 正如Martin Fowler所述,测试金字塔如下所示。用户的目标应当是拥有更多比例的可以快速执行的测试。这意味着相比于其他类型的测试,用户需要拥有更多的单元测试。
    ui.jpg

    图片来源:MartinFowler.com

  • 避免在单元测试中使用数据库。如果可以的话,避免将其用于集成测试。一般来说集成测试需要采用一个替代数据源,通常指向的是一个内存数据库。如果不可避免的需要使用真实数据库进行测试的话,用户需要保证在每次测试之前刷新数据库,确保数据处于已知状态,并且测试不会基于不一致的数据开始。


警告

不要依赖大量的UI测试,UI测试是脆弱的,即他们是经常变动的,并且需要花费大量的精力去维护。笔者建议用户使用像Selenium这样的UI测试框架来规避UI测试过程中遇到的一些问题,例如UI元素在屏幕上位置的变动,UI事件的处理等。

克隆一个生产环境做测试

“存在测试环境的话可能会导致测试通过的系统部署到生产环境时发生故障,因为生产环境可能和测试环境有重大差异。然而,建设一个生产环境副本的成本是非常高昂的。相反,测试环境,或是一个单独的预发布环境('staging')应当被建设成实际生产环境的一个可扩展版本,在节省成本的同时维护技术栈的组成和它们之间的细微差别。在这些测试环境里,人们常常使用服务虚拟化以访问那些超出团队控制的依赖(例如,API,第三方应用,服务,大型机等),它们可能仍然在迭代发展,或是在一个虚拟测试实验室里的配置太过复杂。”

提醒

这是在现实世界的开发中付诸实践时最难实现的一个原则。这需要构建自动化系统来创建并将软件包部署到反映真实生产环境的一个灰度环境里。 除非用户的应用程序是自给自足的,没有任何外部依赖,否则的话这一点很难实现,毕竟,生产环境的复杂度很高。笔者对复杂产品的建议是投入时间和精力借助虚拟化平台或容器平台(如Docker)来复制生产环境。持续交付的流水线可用于将构建部署到这些环境。

获取最新的可交付成果变得很容易

“为测试和其他相关人员提供构建结果,可以在重建不符合需求的功能时减少所需的返工量。此外,早期测试可以减少代码缺陷在部署前的出镜机会。更早地发现错误,在某些情况下,可以减少解决这些错误所需的工作量。每一位程序员都应该从更新仓库项目代码开始新的一天。这样一来,它们都会保持最新。”

提醒

建议使用像Nexus这样的资源仓库来存放最新版本的软件包。通常,存储在这样的资源仓库中的包,它们也是通过版本号进行版本控制的。这使得所有参与者都可以轻松获得当前或过去的任意构建包。

警告

只有主线分支中的构建包才能存放在资源仓库里。 如果不这么做的话,每当有人新建一个正在工作的分支时会导致现有软件包被覆盖。

人人都可以看到最近一次构建的结果

“我们应当能够轻松找出构建是否有问题,如果是的话,谁做了相关的改动。”

提醒
  • 所有的现代CI服务器都有能力展示包含构建状态的仪表盘。正如前面一篇文章所描述的那样,这些也可以被配置成展示其他的一些指标。
  • 所有CI服务器也可以配置为,当构建完成时发送电子邮件通知。笔者建议在构建失败时将电子邮件发送给整个团队,以便可以尽快修复。


警告
  • 一次失败的构建并不是奇耻大辱。每个人都会犯错,开发人员也不能幸免。当构建失败时,我们应当将其视为一个受欢迎的结果,因为该问题被及早地发现了。尽早失败并且尽早修复问题是CI的关键目标。
  • CI不仅仅针对开发人员。通过安装扩展,我们可以从CI系统导出各种指标,它们不仅可以用于提高软件质量,还可以提高开发实践的质量。


自动部署

“大多数CI系统允许在构建完成后运行一些脚本。在大多数情况下,我们可以编写脚本将应用程序部署到每个人都可以查看的一台在线测试服务器。这种思维模式的进一步演变便是持续部署,它需要将软件直接部署到生产环境里,这往往需要额外的自动化手段来防止缺陷或被还原。”

警告
  • 并非所有项目都需要自动部署,尤其是当企业服务器运行在客户站点。项目计划决定了客户站点升级到最新版本的时间,这通常是几个月前就计划好的。如果生产站点是同样是由正在开发该软件的公司自主托管的话,那么对于持续部署系统的投入将更有收益。持续部署是持续集成流程到位并且运转良好时后续的逻辑步骤。
  • 并非所有的提交都能够产出一个可交付的产品。敏捷社区中最常见的误解是认为每个版本都是可交付的产品。可交付的产品与能正常工作的软件的定义完全不同!


小结

笔者希望这些信息可以让用户深入了解一些改进CI流程实施的最佳做法。CI在简化软件开发过程中发挥着重要作用。CI实践的适当调整将提高软件开发过程的整体效率和灵活性。结合这些最佳实践是以更快的上市时间交付高品质软件的诀窍!

原文链接:Continuous Integration Part 3: Best Practices(翻译:Colstuwjx)

0 个评论

要回复文章请先登录注册