《持续交付:发布可靠软件的系统方法》
- 前言
- 软件交付的问题
- 配置管理
- 持续集成
- 测试策略的实现
- 部署流水线解析
- 构建与部署的脚本化
- 提交阶段
- 自动化验收测试
- 非功能需求的测试
- 应用程序的部署与发布
- 基础设施与环境管理
- 数据管理
- 组件和依赖管理
- 版本控制进阶
- 持续交付管理
前言
- 自动化是关键,它让开发人员、测试人员和运营人员能够通过一键式操作完成软件创建和部署过程中的所有常见任务。
- 我们的目标是改变软件交付方式,将其由开发人员的手工操作变成一种可靠、可预期、可视化的过程并在很大程度上实现了自动化的流程,而且它要具备易于理解与风险可量化的特点。
- 敏捷宣言的第一原则:“我们的首要任务是尽早持续交付有价值的软件并让客户满意。”这也反映了这样一个现实:对于成功的软件,首次发布只是交付过程的开始。
- 书中描述的所有技术都与交付软件新版本给客户相关,旨在减少时间和降低风险。这些技术的核心是增加反馈,改进负责交付的开发、测试和运维人员之间的协作。这些技术能确保当需要修改应用程序(也许是修复缺陷,也许是开发新功能)时,从修改代码到正式部署上线之间的时间尽可能短,尽早发现缺陷以便快速修复,并更好地了解与本次修改相关的风险。
软件交付的问题
- 对于应用程序的配置、源代码、环境或数据的每个变更都会触发创建一个新流水线实例的过程。流水线的首要步骤之一就是创建二进制文件和安装包,而其余部分都是基于第一步的产物所做的一系列测试,用于证明其达到了发布质量。每通过一步测试,我都会更加相信这些二进制文件、配置信息、环境和数据所构成的特殊组合可以正常工作。如果这个产品通过了所有的测试环节,那么它就可以发布了。
- 部署流水线的目标有三个。首先,它让软件构建、部署、测试和发布过程对所有人可见,促进了合作。其次,它改善了反馈,以便在整个过程中,我们能够更早地发现并解决问题。最后,它使团队能够通过一个完全自动化的过程在任意环境上部署和发布软件的任意版本。
- 手工部署过程依赖于部署专家。如果专家去度假或离职了,那你就有麻烦了。尽管手工部署枯燥且极具重复性,但仍需要有相当程度的专业知识。若要求专家做这些无聊、重复,但有技术要求的任务则必定会出现各种我们可以预料到的人为失误,同时失眠,酗酒这种问题也会接踵而至。然而自动化部署可以把那些成本高昂的资深高技术人员从过度工作中解放出来,让他们投身于更高价值的工作活动当中。
- 我们的对策就是将测试、部署和发布活动也纳入到开发过程中,让它们成为开发流程正常的一部分。这样的话,当准备好进行系统发布时就几乎很少或不会有风险了,因为你已经在很多种环境,甚至类生产环境中重复过很多次,也就相当于测试过很多次了。而且要确保每个人都成为这个软件交付过程的一份子,无论是构建发布团队、还是开发测试人员,都应该从项目开始就一起共事。
- 软件发布能够(也应该)成为一个低风险、频繁、廉价、迅速且可预见的过程。
- 有用性的一个重要部分是质量。我们的软件应该满足它的业务目的。质量并不等于完美,正如伏尔泰所说“追求完美是把事情做好的大敌”,但我们的目标应该一直是交付质量足够高的软件,给客户带来价值。因此,尽快地交付软件很重要,保证一定的质量是基础。
- 持续集成:每次提交都对应用程序进行构建并测试,这称作持续集成。
- 流程反馈:它是指完全以自动化方式尽可能地测试每一次变更。
- 人力资源是昂贵且非常有价值的,所以我们应该集中人力来生产用户所需要的新功能,尽可能快速地交付这些新功能,而不是做枯燥且易出错的工作。像回归测试、虚拟机的创建和部署这类工作最好都由机器来完成。
- 尽可能全面,即75%左右的代码库覆盖率。只有这样,这些测试通过以后,我们才对自己写的软件比较有信心。
- 对于快速交付高质量的软件来说,基于持续改进的过程是非常关键的。迭代过程有助于为这类活动建立规律性,例如每个迭代至少开一次回顾会议,在会上每个人都应参与讨论如何在下一个迭代中改进交付过程。
- 精益制造的目标是确保快速地交付高质量的产品,它聚焦于消除浪费,减少成本。
- 部署流水线的一个关键点是,它是一个“拉动”(pull)系统,它使测试人员、运维人员或支持服务人员能够做到自服务,即他们可以自行决定将哪个版本的应用程序部署到哪个环境中。
- 减少压力的关键在于拥有一个我们前面所描述的自动化部署过程,并频繁地运行它,当部署失败后还能够快速恢复到原来状态。
- 在每次以同一种方式部署应用软件时,也是验证我们的部署机制是否正确的时机。事实上,向其他任何环境的任何一次部署过程都是生产环境部署的一次演练。
- 如果在软件开发中的某个任务令你非常痛苦,那么解决痛苦的方法只有更频繁地去做,而不是回避。因此,我们应该频繁做集成,事实上应该在每次提交修改后都做集成。持续集成这个实践将频繁集成发挥到了极至,而“持续集成”转变了软件开发过程。持续集成会及时检测到任何一次破坏已有系统或者不满足客户验收测试的提交。一旦发生这种情况,团队就立刻去修复问题(这是持续集成的首要规则)
- 软件发布的可重复性和可靠性来自于以下两个原则:(1)几乎将所有事情自动化;(2)将构建、部署、测试和发布软件所需的东西全部纳入到版本控制管理之中。
- 验收测试是可以自动化的,数据库的升级和降级也是可以自动化的,甚至网络和防火墙配置也是可以自动化的。你应该尽可能自动化所有的东西。
- 如果创建应用程序的说明文档是你的痛点,那么每开发一个功能时就应写好文档,而不是留到最后一起写。把一个功能的说明文档也作为“DONE”的一个验收条件,并尽可能自动化这个过程。
- 越早发现缺陷,修复它们的成本越低。如果在没有提交代码到版本控制之前,我们就能发现并修复缺陷的话,代价是最小的。
- “内建质量”还有另外两个推论。(1)测试不是一个阶段,当然也不应该开发结束之后才开始。如果把测试留在最后,那就为时晚矣,因为可能根本没有时间修复那些刚被发现的问题。(2)测试也不纯粹或主要是测试人员的领域。交付团队的每个人都应该对应用程序的质量负责。
- 理想情况下,团队中的成员应该有共同的目标,并且每个成员应在工作中互相帮助来实现这一目标。无论成功还是失败,其结果都属于这个团队,而非个人。
- 关键在于组织中的每个人都要参与到持续改进过程当中。如果只在自己所在角色的内部进行反馈环,而不是在整个团队范围内进行的话,就必将产生一种“顽疾”:以整体优化为代价的局部优化,最终导致互相指责。
配置管理
- 我们所讨论的有关加快发布周期和提高软件质量的所有实践,从持续集成、自动化测试,到一键式部署,都依赖于下面这个前提:与项目相关的所有东西都在版本控制库中。
- 只要能从版本控制库中取出所需要的一切,就能保证为开发、测试,甚至生产环境提供一个稳定的平台。然后你可以将整个环境(包括配置基线上的操作系统)做成一个虚拟镜像,放在版本控制库中,这可以作为更高级别的保证措施,而且可以提高部署的简单性。
- 这种很长时间才提交的做法是有问题的。因为提交越频繁,越能够体现出版本控制的好处。除非每个人都频繁提交,否则“安全地对系统进行重构”这件事基本上是不可能完成的任务。因为长时间不提交代码会让合并工作变得过于复杂。如果你频繁提交,其他人可以看到你的修改且可与之交互,你也可以清楚地知道你的修改是否破坏了应用程序,而且每次合并工作的工作量会一直很小,易于管理。
- 任何改变应用程序的行为,无论修改了什么,都算是编程,即使只是修改一行配置信息。你进行修改所使用的语言可能或多或少地受到限制,但此时仍是在编程。根据定义,要为用户提供的软件配置能力越强,你能置于系统配置的约束就应越少,而你的编程环境也会变得越复杂。
- 将那些特定于测试环境或生产环境的实际配置信息存放于与源代码分离的单独代码库中通常是非常必要的。因为这些信息与源代码的变更频率是不同的。
- 如果应用程序所依赖的任何部分没有准备好,部署或安装脚本都应该报错,这相当于配置设置的冒烟测试。
- 我们通常在需要时才临时决定如何管理配置信息,其后果是每个应用的配置信息被放在不同的位置,而应用程序又以不同的方式获取这些配置。这会给确定“哪些环境中有哪些配置”带来不必要的困难。
- 环境管理的关键在于通过一个全自动过程来创建环境,使创建全新的环境总是要比修复已受损的旧环境容易得多。
- 即便很微小的变化也可能把环境破坏掉。任何变更在上线之前都必须经过测试,因而要将其编成脚本,放在版本控制系统中。这样,一旦该修改被认可,就可以通过自动化的方式将其放在生产环境中。
- 没有配置管理,根本谈不上持续集成、发布管理以及部署流水线。它对交付团队内部的协作也会起到巨大的促进作用。
持续集成
- 持续集成要求每当有人提交代码时,就对整个应用进行构建,并对其执行全面的自动化测试集合。而且至关重要的是,假如构建或测试过程失败,开发团队就要停下手中的工作,立即修复它。持续集成的目标是让正在开发的软件一直处于可工作状态。
- 持续集成不是一种工具,而是一种实践。它需要开发团队能够给予一定的投入并遵守一些准则,需要每个人都能以小步增量的方式频繁地将修改后的代码提交到主干上,并一致认同“修复破坏应用程序的任意修改是最高优先级的任务”。如果大家不能接受这样的准则,则根本无法如预期般通过持续集成提高质量。
- 单元测试用于单独测试应用程序中某些小单元的行为(比如一个方法、一个函数,或一小组方法或函数之间的交互)。它们通常不需要启动整个应用程序就可以执行,而且也不需要连接数据库(如果应用程序需要数据库的话)、文件系统或网络。它们也不需要将应用程序部署到类生产环境中运行。单元测试应该运行得非常快,即使对于一个大型应用来说,整个单元测试套件也应该在十分钟之内完成。
- 组件测试用于测试应用程序中几个组件的行为。与单元测试一样,它通常不必启动整个应用程序,但有可能需要连接数据库、访问文件系统或其他外部系统或接口(这些可以使用“桩”,即stub技术)。组件测试的运行时间通常较长。
- 验收测试最好采用将整个应用程序运行于类生产环境的运作方式。当然,验收测试的运行时间也较长。一个验收测试套件连续运行一整天是很平常的事儿。
- 在使用持续集成之前,很多开发团队都使用每日构建(nightly build)。当时,微软使用这个实践已经很多年了。谁破坏了构建,就要负责监视后续的构建过程,直至发现下一个破坏了构建的人。
- 在提交代码时,做出了这一代码的开发人员应该监视这个构建过程,直到该提交通过了编译和提交测试之后,他们才能开始做新任务。在这短短几分钟的提交阶段结束之前,他们不应该离开去吃午饭或开会,而应密切注意构建过程并在提交阶段完成的几秒钟内了解其结果。
- 在这里需要澄清一下,我们并不建议你工作到很晚来修复失败的构建,而是希望你有规律地尽早提交代码,给自己足够的时间处理可能出现的问题。或者,你可以第二天再提交。很多有经验的开发人员在下班前一小时内不再提交代码,而是把它作为第二天早上的第一件事情。如果所有手段都不好使,那么把版本控制库中的代码回滚到上一次成功构建的状态,并在本地保留一份失败的代码就可以了。
- 如果某次提交失败了,无论采取什么样的行动,最重要的是尽快让一切再次正常运转起来。如果无法快速修复问题,无论什么原因,我们都应该将它回滚到版本控制库中前一个可工作的版本上,之后再在本地环境中修复它。
- 那些已经成功运行了一段时间的测试失败时,失败的原因可能很难找。这种失败是否真的意味着发现了一个回归问题呢?也许这个测试不再是有效的测试了,也许是因为原有功能因需求变化被改变了。找出真正的失败原因可能需要向很多人了解情况,并且需要花上一段时间,但这是值得的。我们的选择是要么修复代码(如果是回归问题的话),要么修改测试(如果该测试以前的某个假设不成立了),或者删除它(如果被测试的功能已经不存在了)。
- 假如提交代码后,你写的测试都通过了,但其他人的测试失败了,构建结果还是会失败。通常这意味着,你引入了一个回归缺陷。你有责任修复因自己的修改导致失败的那些测试。
- 只有非常高的单元测试覆盖率才有可能保证快速反馈(这也是持续集成的核心价值)。完美的验收测试覆盖率当然也很重要,但是它们运行的时间会比较长。根据我们的经验,能够达到完美单元测试覆盖率的唯一方法就是使用测试驱动开发。尽管我们尽量避免在本书中教条式地提及敏捷开发实践,但我们认为测试驱动开发是持续交付实践成为可能的关键。
- 所谓测试驱动开发是指当开发新的功能或修复缺陷时,开发人员首先要写一个测试,该测试应该是该功能的一个可执行规范。这些测试不但驱动了应用程序的设计,而且既可以作为回归测试使用,也是一份代码的说明文档,描述了应用程序预期的行为。
- 重构是指通过一系列小的增量式修改来改善代码结构,而不会改变软件的外部行为。通过持续集成和测试驱动开发可以确保这些修改不会改变系统的行为,从而使重构成为可能。
- 只要你指定某个仓库作为主库(master),每次更改这个仓库就触发持续集成服务器上的一次构建,并让每个人都将其修改推送到这个仓库中来实现共享。很多使用分布式系统的项目都使用这种方式,而且非常成功。
- 持续集成的使用会为团队带来一种开发模式上的转变。没有持续集成的话,直到验证前,应用程序可能一直都处于无法工作的状态,而有了持续集成之后,应用程序就应该是时刻处于可工作状态的了,虽然这种自信取决于自动化测试覆盖率。
测试策略的实现
- 测试是跨职能部门的活动,是整个团队的责任,应该从项目一开始就一直做测试。
- 质量内嵌是指从多个层次(单元、组件和验收)上写自动化测试,并将其作为部署流水线的一部分来执行,即每次应用程序的代码、配置或环境以及运行时所需软件发生变化时,都要执行一次。
- 手工测试也是质量内嵌的关键组成部分,如演示、可用性测试和探索性测试在整个项目过程中都应该持之以恒地做下去
- 测试策略的设计主要是识别和评估项目风险的优先级,以及决定采用哪些行动来缓解风险的一个过程。
- 回归测试是自动化测试的全集。它们用来确保任何修改都不会破坏现有的功能,还会让代码重构变得容易些,因为可以通过回归测试来证明重构没有改变系统的任何行为。
- 一般我们将代码覆盖率高于80%的测试视为“全面的”测试,但测试质量也非常重要,单单使用覆盖率这一指标是不够的。
- 一个很好的经验法则就是,一旦对同一个测试重复做过多次手工操作,并且你确信不会花太多时间来维护这个测试时,就要把它自动化
- 一般来说,验收测试都是端到端的测试,并运行在一个与生产环境相似的真实工作环境中。
- 单元测试不应该访问数据库、使用文件系统、与外部系统交互。或者说,单元测试不应该有系统组件之间的交互。这会让单元测试运行非常快,因此可以得到更早的反馈,了解自己的修改是否破坏了现有的任何功能。这些测试也应该覆盖系统中每个代码分支路径(最少达到80%)。这样,它们就组成了回归测试套件的主要部分。
- 部署测试用于检查部署过程是否正常。换句话说,就是应用程序是否被正确地安装、配置,是否能与所需的服务正确通信,并得到相应的回应。
- 探索性测试是一个创造性的学习过程,并不只是发现缺陷,它还会致使创建新的自动化测试集合,并可以用于覆盖那些新的需求。
- 非功能测试是指除功能之外的系统其他方面的质量,比如容量、可用性、安全性等。
- 桩(stub)是在测试中为每个调用提供一个封装好的响应,它通常不会对测试之外的请求进行响应,只用于测试。
- 盲目地用书写差劲的验收条件实现自动化测试是产生不易维护的验收的测试套件的主要原因之一。
- 假如你发现对同一个功能重复进行了多次的手工测试,就要判断一下这个功能是否还会被修改。如果不会的话,就将这个测试自动化。
- 坐下来与用户一起识别系统中高价值的功能是非常重要的。利用前面一节所说的技术,创建一套广泛的自动化测试,覆盖这些高价值的核心功能。
- 对于遗留系统来说,这些覆盖核心功能的测试就是非常重要的冒烟测试了。
- 与没有考虑可测试性的那些系统相比,在设计时就考虑到可测试性的系统,其标准组件化的倾向更强,而且更容易测试。
- 当软件需要在很多不同的环境上运行时,情况就不同了。此时,自动化测试和类生产环境的自动化部署相结合,会给项目带来巨大的价值,因为可以把脚本直接指向需要测试的环境,从而节省大量的手工测试时间与精力。
- 在组件测试和集成测试之间的分界线并不十分清晰(尤其当“集成测试”这个词被赋予了太多的意义)。我们所说的“集成测试”是指那些确保系统的每个独立部分都能够正确作用于其依赖的那些服务的测试。
- 这些自动化集成测试可以当成向生产环境部署系统时的冒烟测试,也可以作为一种诊断方法来监控生产环境中的系统行为。
- 我们可以把缺陷分为严重(critical)、阻塞(blocker)、中(medium)和低(low)四个级别。
- 测试主要是建立反馈环,而这个反馈环会驱动开发、设计和发布等活动。将测试推迟到项目后期的计划最终都会失败,因为它破坏了产生高质量、高生产率,以及(最重要的)反映项目进展情况的反馈环。
部署流水线解析
- 持续集成主要关注于代码是否可以编译成功以及是否可通过单元测试和验收测试。但持续集成并不足以满足我们的需要。持续集成的主要关注对象是开发团队。持续集成系统的输出通常作为手工测试流程和后续发布流程的输入
- 由于很容易将应用程序部署到测试环境中,所以团队可以同时得到软件功能和部署流程两个方面的快速反馈。
- 从精益的角度来看,我们实现了一个“拉式系统”(pull system),即测试团队只要自己单击按钮,就能将某个特定的软件版本部署到测试环境中。运维人员也可以通过单击一下按钮就把软件部署到试运行环境和生产环境中。
- 部署流水线是指软件从版本控制库到用户手中这一过程的自动化表现形式。
- 二进制包应该只在构建流水线的提交阶段生成一次。这些二进制包应该保存在文件系统的某个位置上,让流水线的后续阶段能够轻松地访问到这个位置,但要注意不要放在版本控制库中,因为它只是一个版本的衍生品,并不是原生态的定义。
- 我们不想在那些明显有问题的版本上花时间和精力,所以当开发人员提交变更到版本控制系统后,我们希望尽快地评估一下这个最新版本。提交者要一直等到构建结果,然后才能做下一项工作。
- 当缺陷还比较容易修复时,尽快得到反馈是非常重要的,而不应花更大的代价得到全面的反馈。
- 对于在这个待发布的候选版本,第一阶段的成功是一个重要的里程碑,它是这个部署流水线的一个关卡。一旦通过这个关卡,开发人员就被从上一个任务中释放出来,开始做下一个任务了。然而,他们仍旧有责任监视后续阶段的运行状况。即使后续阶段出了问题,修复失败的构建仍旧是开发团队的首要任务。我们赌自己能成功,可一旦赌输了,也已准备好去偿还技术债。
- 生产环境中的大多数问题往往是由不充分的控制导致的。正如我们在第11章中所讲的,生产环境应该是完全受控的,即对生产环境的任何修改都应该通过自动化过程来完成。这不仅包括应用程序的部署,还包括对配置、软件栈、网络拓扑以及状态的所有修改。只有在这种方式下,我们才可能对它们进行可靠地审计和问题诊断,并在可预计的时间内修复它们。随着系统复杂性的增加,不同类型服务器的增多,以及不断提高的性能需求,我们就更需要这种程度的控制力。
- 实现部署流水线的第一步是将构建和部署流程自动化。构建过程的输入是源代码,输出结果是二进制包。
- 每当有人提交后,持续集成服务器就应执行构建,持续集成服务器应该监视版本控制系统,每当发现有新提交的代码时,就签出或更新源代码,运行自动化构建流程,并将生成的二进制包放在文件系统的某个地方,使整个团队都能通过持续集成服务器的用户界面获取。
- 部署活动可能包含:(1)为应用程序打包,而如果应用程序的不同组件需要部署在不同的机器上,就要分别打包;(2)安装和配置过程应该实现自动化;(3)写自动化部署测试脚本来验证部署是否成功了。部署流程的可靠性是非常重要的,因为它是自动化验收测试的前提条件。
- 在开发构建和部署系统的过程中,一定要确保遵循前面说过的那些原则,如只生成一次二进制包,将配置信息与二进制包分离,以便在不同环境的部署中可以使用相同的二进制包。这能确保配置管理有一个健全的基础。
- 开发部署流水线的下一步就是实现全面的提交阶段,也就是运行单元测试、进行代码分析,并对每次提交都运行那些挑选出来的验收测试和集成测试
- 因为单元测试并不需要访问文件系统或数据库(与之对应的是组件测试),所以运行速度应该很快。这也是构建应用程序之后就直接运行单元测试的原因。与此同时,还可以运行一些静态分析工具,得到一些有用的分析数据,比如代码风格、代码覆盖率、圈复杂度、耦合度等。
- 反馈是所有软件交付流程的核心。改善反馈的最佳方法是缩短反馈周期,并让结果可视化。你应该持续度量,并把度量结果以一种让人无法回避的方式传播出去。
构建与部署的脚本化
- 所有构建工具都有一个共同的核心功能,即可以对依赖关系建模。在执行过程中,它能以正确的顺序执行一系列的任务,计算如何达到你所指定的目标,而且被依赖的任务也仅需要运行一次。
- 这种流行的“惯例胜于配置”(convention over configuration)的原则意味着,只要项目按Maven指定的方式进行组织,它就几乎能用一条命令执行所有的构建、部署、测试和发布任务,却不用写很多行的XML。
- 功能验收测试脚本会调用部署工具,将应用程序部署到适当环境中,并准备相关数据,之后再运行验收测试。你还可再用一个脚本运行任何非功能测试,比如压力测试和安全测试。
- 事实上,当你查看我们的部署系统时会发现,它只是由一组非常简单的、增量的步骤组成的复杂系统,而这些步骤也是随着项目的进行不断完善的。我们想说的是,并不是完成所有的步骤之后才能获得价值。事实上,当你第一次写了一个脚本用于在本地的开发环境上部署应用程序,并将其分享给整个团队时,就已经节省了很多开发人员的时间。
- 我们应该遵循Java命名习惯,如包名用PascalCase方式,而类名使用camelCase方式。在代码提交阶段做代码分析时应利用一些开源工具(比如CheckStyle或FindBugs)来做检查,迫使大家遵循这些命名习惯
- 单元测试应该放在与包名相对应的目录中。也就是说,某个类的测试应该与该类放在同一个包中
- 环境管理的核心原则之一就是:对测试和生产环境的修改只能由自动化过程执行。也就是说,我们不应该手工远程登录到这些环境上执行部署工作,而应该将其完全脚本化。
- 构建中最常见的错误就是默认使用绝对路径。这会让构建流程和某台特定机器的配置形成强依赖,从而很难被用于配置和维护其他服务器。
- 假如我们已经让你深入理解了构建脚本化,并使你了解到各种各样的可能性的话(更重要的是激发你走向自动化),我们的目的也就达到了。
提交阶段
- 对于开发人员来说,提交阶段是开发环节中最重要的一个反馈循环。它会为开发人员引入的最常见错误提供迅速反馈。
- 修复那些在提交阶段发现的问题,要比修复那些由后续运行大量测试的阶段发现的问题简单得多。
- 只有在某个错误让提交阶段的其他任务无法执行时,我们才会让提交阶段停下来,比如编译错误,否则就直至提交阶段全部运行完后,才汇总所有的错误和失败报告,以便可以一次性地修复它们。
- 有人认为,在提交阶段结束时,应该提供更丰富的信息,比如关于代码覆盖率和其他度量项的一些图表。实际上,这些信息可以使用一系列阈值聚合成一个“交通灯信号”(红色、黄色、绿色),或者浮动的衡量标度。比如,当单元测试覆盖率低于60%就令提交阶段失败,但是如果它高于60%,低于80%的话,就令提交阶段成功通过,但显示成黄色。
- 随着项目的进行,要不断努力地改进提交阶段脚本的质量、设计和性能。一个高效、快速、可靠的提交阶段是提高团队生产效率的关键,所以只要花点儿时间和精力在这上面,让它处于良好的工作状态,就会很快收回这些投入成本。
- 不能低估专家们的专业知识,但他们的目标应该是建立并使用良好的结构、模式和技术,并将他们的知识传授给交付团队。一旦建立了这些基本规则,只有对脚本结构进行较大修改时才需要他们的专业知识,而日常构建维护工作不应该由他们来做。
- 提交测试中,绝大部分应由单元测试组成。单元测试最重要的特点就是运行速度非常快。有时候,我们会因为测试套件运行不够快而令构建失败。第二个重要的特点是它们应覆盖代码库的大部分(经验表明一般为80%左右),让你有较大的信心,能够确定一旦它通过后,应用程序就能正常工作。当然,每个单元测试只测试应用程序的一小部分,而且无须启动应用程序。因此,根据定义,单元测试套件无法给你绝对信心说“应用程序可以工作”,而这正是部署流水线后续部分的任务。
- 我们建议尽量消除提交阶段测试中的异步测试。依赖于基础设施(比如消息机制或是数据库)的测试可以算做组件测试,而不是单元测试。更复杂、运行得更慢的组件测试应该是验收测试的一部分,而不应该属于提交阶段。
- 打桩是指利用模拟代码来代替原系统中的某个部分,并提供已封装好的响应。桩并不对外界作出响应
- 在一些简单的断言中,你能指定测试中期望该模拟类作出什么行为。这是模拟技术和桩技术的根本不同。使用桩时,我们不需要关心桩是如何被调用的,而使用模拟对象时,可以验证我们的代码是否以期望的方式与模拟对象进行交互。
- 很容易落入一个陷阱,即为了支撑测试,精心地建立起一堆难以理解和维护的数据结构。理想的测试应该能很容易和快速地进行测试准备,而清理工作也应该更快、更容易。对于结构良好的代码来说,其测试代码往往也非常整洁有序。如果测试看起来繁琐复杂,那可能是系统设计有问题。
自动化验收测试
- 一旦正确实施自动化验收测试,你就是在测试应用程序的业务验收条件,即验证应用程序是否为用户提供了有价值的功能。验收测试通常是在每个已通过提交测试的软件版本上执行的。
- 对于一个单独的验收测试,它的目的是验证一个用户故事或需求的验收条件是否被满足
- “验收与我们的单元测试有什么区别?”其不同点在于验收测试是针对业务的,而不是面向开发的。它能在一个类生产环境中的应用程序运行版本上一次性地测试所有的故事。单元测试的确是自动化测试策略的关键部分,但是,它通常并不足以使人们确信程序能够发布。而验收测试的目标就是要证明应用程序的确实现了客户想要的,而不是以编程人员所认为的正确方式来运行的。虽然有时单元测试也会实现同样的目标,但并不总是这样的。
- 除验收测试外,没有哪种测试能够基本上代替生产环境中的实际运行来证明软件能为客户提供他们所期望的业务价值。单元测试和组件测试都不测试用户场景,因此也无法发现那种用户与应用程序进行一系列交互后呈现出来的缺陷。而验收测试就是为这而设计的.
- 验收测试在以下几个方面也表现出不俗的查错能力:线程问题、以事件驱动方式实现的应用程序出现的紧急行为(emergent behavior),以及由架构问题或环境及配置问题造成的其他类型的bug。这类缺陷很难通过手工测试发现,更不用说单元测试和组件测试了。
- 选择放弃自动化验收测试的团队会令测试人员的负担非常重,测试人员必须在恼人且重复的回归测试上花费相当多的时间。
- 根据我们的经验,与完全由开发人员编写的自动化验收测试相比,那些有测试人员参与编写的自动化验收测试能更好地发现用户场景中的缺陷。
- 将可测试性铭记在心,写出来的应用程序就会有一个API,使GUI和测试用具(test harness)都能用它来驱动应用程序。如果应用程序能够做到这一点的话,我们建议直接基于业务层执行测试,这是一个合理的策略。唯一的要求就是开发团队在这方面的纪律性,即让表现层只负责展现,不要涉足业务领域或应用逻辑。
- 在迭代交付方法中,分析人员会花大量时间定义验收条件。团队用这些验收条件来评判某个具体需求是否被满足。
- 行为驱动开发的核心理念之一就是验收测试应该以客户期望的应用程序行为的方式来书写。这样,就可以拿这些写好的验收条件直接在应用程序之上运行,来验证它是否满足规格说明了。
- 测试用例的工作就是让应用程序达到“假如”中所述的状态,然后执行“当……”中所描述的动作,最后验证应用程序是否处于“那么”中所描述的状态。
- 原子测试会创建它所需要的一切,并在运行后清理干净。除了是否成功以外,不会留下其他东西。
- 刚接触自动化测试的人会发现,想让代码可测试,必须修改对它的设计,事实的确如此。
- 自动化测试会给你压力,让你的代码更趋向于模块化和更好的封装性。但是如果你通过破坏封装性让它变得可测试,那么通常就会错过达到同一目的的好方法。
- 自动化验收测试与用户验收测试并不完全一样。其中一个不同点就是:自动化验收测试不应该运行在包含所有外部系统集成点的环境中。相反,应该为自动化验收测试提供一个受控环境,并且被测系统应该能在这个环境上运行。这里所说的“受控”是指,可以为每个测试创建正确的初始化状态。如果与真正的外部系统集成,我们很可能就无法做到这一点。
- 令验收测试失败的构建版本不能被部署。在部署流水线模式中,只有已经通过这一阶段的候选发布版本才能走向后续阶段。而后续阶段常常被认为是需要人为评判的:在大多数项目中,如果某个候选发布版本无法通过容量测试,就会有人来决定这次失败是否足以严重到要取消这个候选版本的发布资格,还是让它继续走下去。可是,对于验收测试,不应该提供这种人为评定的机会。如果成功,就可以继续,如果失败,就不能向前。
- 不断运行这些复杂的验收测试,的确会花费开发团队很多时间。然而,根据我们的经验,这种成本投入是一种投资,会节省很多倍的维护成本。当对应用程序进行大范围修改时,它就是一张防护网,而且软件质量也会得到保证。这也符合我们的总原则:将流程中的痛点尽量提前。
- 当某个验收测试失败时,团队要停下来立即评估问题。它是一个脆弱的测试,还是由于环境配置问题,或者是由于应用程序的某个修改使原有的假设不成立了,还是一个真正的失败?然后,让某人立即采取行动,使测试通过。
- 我们认为,持续地关注维护验收测试套件,以保持它的良好结构和连贯性是非常重要的,但是自动化验收测试的全面性要比测试在10分钟内运行完成更重要。
- 通常,验收测试套件花几个小时而不是几分钟才能运行完。这是可以接受的,很多项目的验收测试阶段都要花几个小时,但也运行良好。但是,仍旧有办法可以提高效率。有一系列的技术能用来缩短从验收测试阶段得到运行结果的时间,从而提高团队的整体效率。
- 自动化验收测试通常要比单元测试复杂,需要更多的时间进行维护。而且,由于它在修复某个失败与使所有验收测试套件成功通过之间那种固有的滞后性,所以与单元测试相比,它处于失败状态的时间要长一些。
- 虽然在我们参与的项目中,确保验收测试持续运行是一项很困难的工作,而且带来了一些复杂问题,但是,我们从来没有后悔使用验收测试。它使我们能对系统安全地进行大规模重构。我们还坚信,在开发团队中鼓励关注这种测试的是软件成功交付的有力武器。
- 手工测试是软件行业中的一种基准,并且常常是一个团队进行测试的唯一形式。我们发现,手工测试的成本不但极其昂贵,而且也不足以确保生产出高质量的软件。当然,手工测试有其自己的位置,如探索性测试、易用性测试和用户验收测试和演示。人类生来就不适合做那种索然无味的、需要不断重复但却非常复杂的工作,然而,不幸的是,这些恰恰都是做手工回归测试所需要的。这种低质量过程必然生产出低质量的软件。
非功能需求的测试
- “性能”是对处理单一事务所花时间的一种度量,既可以单独衡量,也可以在一定的负载下衡量。“吞吐量”是系统在一定时间内处理事务的数量,通常它受限于系统中的某个瓶颈。在一定的工作负载下,当每个单独请求的响应时间维持在可接受的范围内时,该系统所能承担的最大吞吐量被称为它的容量。
- 把非功能需求与功能需求区别对待,就很容易把它从项目计划中移除,或者不给予它们足够的分析。然而,这可能就是一个灾难,因为非功能需求常常是项目风险的来源之一。在交付过程的后期才发现应用程序因基本的安全漏洞或很差的性能而导致项目无法验收,这种常见现象会导致项目推迟交付甚至被取消。
- 过早且过分地关注应用程序的容量优化是低效且昂贵的。而且,最终交付的应用系统也很少是高性能的。更糟糕的是,它甚至可能让项目无法交付。
- 现代软件系统中,最昂贵的是网络通信或磁盘存储。在性能和应用程序的稳定性方面,跨进程或网络边界的通信是昂贵的,所以这类通信应该尽量最小化。
- 为了能够获得项目成功,必须避免两个极端:一是假设自己能在项目后期解决所有容量问题;二是因害怕未来可能出现的容量问题而写一些具有防范性的、过分复杂的代码。
- 如果对于应用程序来说,性能或吞吐量是一个重要指标的话,我们就需要用一些测试来断言系统能够满足业务需求,而不是通过技术经验来猜测某个特定组件的吞吐量应该是多少。
- 假如对某应用程序来说,容量或性能是一个非常关键的问题,那么就一定要有所投入,为该系统的核心部分准备一个生产环境的副本。使用相同的软硬件规格要求,遵循我们关于如何管理配置信息的建议,以确保每个环境中都使用相同的配置文件,包括网络配置、中间件及操作系统的配置。
- 要记住:代码的修改对系统容量的影响与其对功能的影响一样重要。当做了修改之后,要尽早掌握容量会下降多少,这样就能快速且有效地修复它。这就要在部署流水线中加入一个阶段,即容量测试阶段。
- 我们要一直遵守这样的格言,即做最少的工作达到我们的目标,这也是YAGNI(“You Ain’t Gonna NeedIt”)原则所暗示的。YAGNI提醒我们,增加防御性行为都有可能成为浪费。如果遵循高德纳的格言,应该直到明确需要优化而且到了最后时刻才做优化。另外,还要基于应用程序运行时分析结果,直接解决最重要的瓶颈问题。
应用程序的部署与发布
- 创建发布策略的最重要部分是在项目计划阶段就与应用程序的所有干系人会面。讨论的关键在于,要对整个应用程序的生命周期中的部署与维护达成共识。然后把这个共识作为发布策略写下来。在整个生命周期中,干系人应该对该文档进行更新和维护。
- 要让软件的部署活动能以一种可靠且一致的方式进行,其关键在于每次部署时都使用同样的实践方法,即使用相同的流程向每个环境进行部署,包括生产环境在内。在首次向测试环境部署时就应该使用自动化部署。写个简单的脚本来做这件事,而不是手工将软件部署到环境中。
- 我们认为,项目首个迭代的主要目标之一就是在迭代结束时,让部署流水线的前几个阶段可以运行,且能够部署并展示一些成果,即使可展示的东西非常少。尽管我们不建议让技术价值的优先级高于业务价值的优先级,但此时是个例外。你可以把这一策略看做实现部署流水线的“抽水泵”。
- 当制定发布回滚计划时,需要遵循两个通用原则。首先,在发布之前,确保生产系统的状态(包括数据库和保存在文件系统中的状态)已备份。其次,在每次发布之前都练习一下回滚计划,包括从备份中恢复或把数据库备份迁移回来,确保这个回滚计划可以正常工作。
- 金丝雀发布就是把应用程序的某个新版本部署到生产环境中的部分服务器中,从而快速得到反馈。就像发现一只煤矿坑道里的金丝雀那样,很快就会发现新版本中存在的问题,而不会影响大多数用户。像蓝绿部署一样,你要先部署新版本到一部分服务器上,而此时用户不会用到这些服务器。然后就在这个新版本上做冒烟测试,如果必要,还可以做一些容量测试。最后,你再选择一部分用户,把他们引导到这个新版本上。有些公司会首先选择一些“超级用户”来使用这个新版本。甚至可以在生产环境中部署多个版本,根据需要将不同组的用户引导到不同的版本上。
基础设施与环境管理
- 强调合作是DevOps运动的核心原则之一。DevOps运动的目标是将敏捷方法引入到系统管理和IT运营世界中。这场运动的另一个核心原则是,利用敏捷技术对基础设施进行有效管理。
数据管理
- 测试独立性是指确保每个测试都具有原子性。也就是说,每个测试不应该用其他测试的结果建立它的初始状态,并且其他测试也不应该以任何形式影响该测试的成功或失败
- 我们通过测试来断言我们所开发的应用程序的行为符合我们期望的结果。我们运行单元测试来避免刚做的修改破坏已有的应用程序。我们运行验收测试来断言应用程序交付了用户所期望的价值。我们执行容量测试来断言应用程序满足我们的容量需求。可能,我们还会通过运行一套集成测试来确认应用程序与其依赖的第三方服务可以正常通信。
组件和依赖管理
- 对于“应用程序功能的可用性”这个问题,持续集成可以给你某种程度上的自信。而部署流水线(持续集成的扩展)用于确保软件一直处于可发布状态。但是,这两个实践都依赖于一件事,即主干开发模式[插图]。
- 为了在变更的同时还能保持应用程序的可发布,有如下四种应对策略。❑ 将新功能隐蔽起来,直到它完成为止。❑ 将所有的变更都变成一系列的增量式小修改,而且每次小的修改都是可发布的。❑ 使用通过抽象来模拟分支(branch by abstraction)的方式对代码库进行大范围的变更。❑ 使用组件,根据不同部分修改的频率对应用程序进行解耦。
- “通过抽象来模拟分支”是一次性实现复杂修改或分支开发的替代方法。它让团队在持续集成的支撑下持续开发应用程序的同时替换其中的一大块代码,而且这一切都是在主干上完成的。如果代码库的某一部分需要修改,首先要找到这部分代码的入口(一个缝隙),然后放入一个抽象层,让这个抽象层代理对当前实现方式的调用。然后,开发新的实现方式。到底使用哪种实现方式由一个配置选项来决定,可以在部署时或者运行时对这个选项进行修改。
- 区分组件和库:库是指团队除了选择权以外,没有控制权的那些软件包,它们通常很少更新。相反,组件是指应用程序所依赖的部分软件块,但它通常是由你自己的团队或你公司中的其他团队开发的。组件通常更新频繁。
- 一个相当有争议的陈述是这样的:“组件是可重用的代码,它可以被实现了同样API的其他代码所代替,同时可独立部署,并封装了一些相关的行为和系统的部分职能。”
- 依据功能领域而不是组件来组建团队确保了每个人都有权力修改代码库的任何部分,同时在团队之间定期交换人员,确保团队之间有良好的沟通。这种方法还有一个好处,即确保所有的组件能组合在一起正常工作是所有人的责任,而不只是最后负责集成的那个团队的责任。
版本控制进阶
- 分支的唯一目的就是可以对代码进行增量式或“通过抽象来模拟分支”方式的修改。
- 如果一个团队的不同成员在不同分支或流上工作的话,那么根据定义,他们就不是在做持续集成。让持续集成成为可能的一个最重要实践就是每个人每天至少向主干提交一次。因此,如果你每天将分支合并到主线一次(而不只是拉分支出去),那就没什么。如果你没这么做,你就没有做持续集成。
- 在开发过程中,通过频繁向主干提交的方式做这种增量式修改几乎总是最正确的做事方法,所以请一直把它作为备选列表中的第一项。
- 从一个代码库上挑选一些变更发送给另一个代码库,这个过程叫做摘樱桃(cherry-picking)。也就是说,与其总是要合并整个分支,不如只合并想要的那些特性.
持续交付管理
- 通过确保交付团队能得到应用程序在类生产环境上的不断反馈,是部署流水线达成“执行度”这个目标的方法和手段。部署流水线使交付流程更加透明,来帮助团队达成符合度。
- 团队组建与磨合常常会经历五个阶段:创建期(forming)、风暴期(storming)、规范期(norming)、运转期(performing)和调整/重组期(mourning/reforming)
- “启动阶段”是对开始写产品代码前这段时间最简单的描述。一般来说,此时会对需求进行收集和分析,并对项目的范围和计划进行初步规划。
- 迭代开发提供的是项目进展情况的客观度量,它是用开发团队能够供给用户可工作的软件,并且该软件完成了多少被用户认可,满足用户目标的功能来衡量项目进度的。
- 对于每个项目的成功来说,管理都是至关重要的。良好的管理所创建的流程令软件更高效地交付,同时确保风险被适当地管理,规章制定被严格遵守。