《持续交付 发布可靠软件的系统方法》读书笔记
当更改项目状态(向版本控制库的一次提交)时,提交阶段就开始了。当它结束时,你要么得到失败报告,要么得到后续测试和发布阶段可用的二进制产物和可部署程序集,以及关于当前应用程序状态的报告。理想情况下,提交阶段的运行应该少于五分钟,一定不会超过十分钟。
提交阶段也是应该开始构建部署流水线的起点。
对于开发人员来说,提交阶段是开发环节中最重要的一个反馈循环。它会为开发人员引入的最常见错误提供迅速反馈。提交阶段的结果是每个候选发布版本的生命周期中一个重大的事件。这一阶段的成功是唯一进入部署流水线,启动该软件交付流程的途径。
提交阶段的原则和实践
提交阶段的目标是在那些有问题的构建引起麻烦之前,就把它们拒之门外。提交阶段的首要目标是要么创建可部署的产物,要么快速失败并将失败原因通知给团队。
提供快速有用的反馈
提交测试的失败通常是由以下三个原因引起的:
由于语法错误导致编译失败;
由于语义错误导致一个或多个测试失败;
由于应用程序的配置或环境方面(包括操作系统本身)的问题引起;
无论是什么原因导致了失败,提交测试一结束,就要通知开发人员,并提供简明的失败原因报告,比如失败测试的列表、编译错误或其他错误清单。
何时令提交阶段失败
传统上讲,当出现下列任一情况时,提交阶段就应该失败,即出现编译错误、测试失败,或者环境问题,否则就应该让提交阶段成功通过并报告一切 OK。
关于“提交阶段只有成功和失败两种状态的限制是否太严格了”有很多争论。有人认为,在提交阶段结束时,应该提供更丰富的信息,比如关于代码覆盖率和其他度量项的一些图表。实际上,这些信息可以使用一系列阈值聚合成一个“交通灯信号”(红色、黄色、绿色),或者浮动的衡量标度。比如,当单元测试覆盖率低于60%就令提交阶段失败,但是如果它高于60%,低于80%的话,就令提交阶段成功通过,但显示成黄色。
我们的纪律是如果提交阶段失败,交付团队就要立即停下手上的工作,把它修复。
我们强烈建议在提交阶段持续检查应用程序的质量,并在恰当的时候考虑加强代码质量的度量。
精心对待提交阶段
提交阶段中有构建用的脚本和运行单元测试、静态分析等的脚本。这些脚本需要小心维护,就像对待应用程序的其他部分一样。
随着项目的进行,要不断努力地改进提交阶段脚本的质量、设计和性能。一个高效、快速、可靠的提交阶段是提高团队生产效率的关键,所以只要花点儿时间和精力在这上面,让它处于良好的工作状态,就会很快收回这些投入成本。
让开发人员也拥有所有权
交付团队对提交阶段(也包括流水线基础设施的其他部分)拥有所有权是至关重要的,这与交付团队的工作和生产效率是紧密联系在一起的。如果你设置了人为障碍,使开发人员不能快速有效地作出修改,就会减缓他们的工作进程,并在其前进的道路上埋下地雷。
在某些组织中会有一支专家团队,团队成员都精通创建有效且模块化的构建流水线,并且擅长管理这些脚本的运行环境。如果真的只有那些专家才有权维护持续集成系统的话,那就是一种失败的管理方式。
如果必要的话,即使是很普通的变更(比如增加新的库文件和配置文件等)也都应该由一起工作的开发人员和运维人员来执行。
开发人员和运维人员都必须要习惯构建系统的维护工作,而且要对其负责。
在超大项目团队中指定一个构建负责人
在小团队或只有二三十人的团队中,自组织就可以了。如果构建失败了,通常很容易在这种规模的团队中确定谁(一位或多位负责人)该负责修复它,如果他没进行修复的话则提醒一下他,如果他在进行修复,就帮他一下。
但在大团队中,这并不总是一件容易的事。此时,让某个(或多个)人扮演构建负责人的角色是必要的。他们不但要监督和指导对构建的维护,而且还要鼓励和加强构建纪律。如果构建失败,构建负责人要知会当事人并礼貌地(如果时间太长的话,不礼貌也没问题)提醒他们为团队修复失败的构建,否则就将他们的修改回滚。
构建负责人不应该是由固定的人担任。团队成员应该轮流担当,比如每星期轮换一次。这个纪律不错,能让每个人都学到一些经验。无论怎么说,想一直做这项工作的人还是不多的。
提交阶段的结果
与部署流水线的所有阶段一样,提交阶段既有输入,也有输出。输入是源代码,输出是二进制包和报告。
产生的报告包括测试结果(假如测试失败,这些结果是找出哪里出了错的重要信息)和代码库的分析报告。分析报告可能包括测试覆盖率、圈复杂度、复制/粘贴分析、输入和输出耦合度以及其他有助于建立健康代码库的度量项。提交阶段生成的二进制包应该在该部署流水线的实例中一直被重用,而且(如果可能)最后还会发布给用户。
交付团队的某个人提交了一次修改;
持续集成服务器运行提交阶段;
成功结束后,二进制包和所有报告和元数据都被保存到制品库中;
持续集成服务器从制品库中获取提交阶段生成的二进制包,并将其部署到一个类生产测试环境中;
持续集成服务器使用提交阶段生成的二进制包执行验收测试;
成功完成后,该候选发布版本被标记为“已成功通过验收测试”;
测试人员拿到已通过验收测试的所有构建的列表,并通过单击一个按钮将其部署到手工测试环境中;
测试人员执行手工测试;
一旦手工测试也通过了,测试人员会更新这个候选发布版本的状态,指示它已经通过手工测试了;
持续集成服务器从制品库中拿到通过验收测试(根据部署流水线的配置,也可能是手工测试)的最新候选发布版本,将其部署到生产测试环境;
对这个候选发布版本进行容量测试;
如果成功了,将这个候选版本的状态更新为“已通过容量测试”;
如果部署流水线中还有后续阶段的话,一直重复这种模式;
一旦这个候选发布版本通过了所有相关阶段,把它标记为“可以发布”,并且任何被授权的人都能将其发布,通常是由质量保证人员和运维人员共同批准;
一旦发布以后,将其标记为“已发布”;
提交测试套件的原则与实践
避免用户界面
用户界面测试的困难来自两方面。首先,它会涉及很多组件或软件的多个层次。这样是容易出问题的,因为要花很多时间和精力去准备各种各样的组件或数据,才能让测试运行起来。其次,用户界面是提供给用户手工操作的,而手工操作的速度与计算机操作的运行速度相比,是相当慢的。
使用依赖注入
依赖注入(或控制反转)是一种设计模式,用于描述如何从对象外部建立对象间的关系。显然,只有在使用面向对象语言时才能用上它。
这种技术不但是构建灵活的模块化软件的很好的方法,而且它还能让测试变得很容易,只需要测试必要的类,那些依赖包就不再是包袱了。
避免使用数据库
首先,这种测试运行得非常慢。当想重复测试,或者连续运行几次相似的测试时,这种有状态的测试就是个障碍。
其次,基础设施准备工作的复杂性令这种测试方法的建立和管理更加复杂。
最后,如果从测试中很难消除数据库依赖的话,这也暗示着,你的代码在通过分层进行复杂性隔离方面做得不好。这也使得可测试性和 CI 在团队身上施加了一种微妙的压力,迫使其开发出更好的代码。
在单元测试中避免异步
在单个测试用例中的异步行为会令系统很难测试。最简单的办法就是通过测试的切分来避免异步,这样就能做到:一个测试运行到异步点时,切分出来的另一个测试再开始执行。
我们建议尽量消除提交阶段测试中的异步测试。依赖于基础设施(比如消息机制或是数据库)的测试可以算做组件测试,而不是单元测试。更复杂、运行得更慢的组件测试应该是验收测试的一部分,而不应该属于提交阶段。
使用测试替身
理想的单元测试集中在很小且紧密相关的代码组件上,典型的就是单个类或一小组极其相关的类。
如果系统设计得比较好,每个类都比较小,并通过与其他类的交互完成其运行目的。这是良好封装设计的核心,即每个类都不对外暴露它是如何达到其目标的。问题是,在这种设计得比较好的模块化系统中,为了测试一个在关系网中心的某个类,可能需要对它周边的很多类进行冗长的设置。解决办法就是与其依赖类进行模拟交互。
最少化测试中的状态
理想情况下,单元测试应聚焦于断言系统的行为。
设法让测试中的这种对状态的依赖最小化。你可能无法从根本上消除它,但为了运行测试,持续关注“如何降低要构造的测试环境的复杂性”是合理的。如果测试变得越来越复杂,很可能是由于代码结构问题引起的。
时间的伪装
对于所有基于时间的系统行为,我们的做法是将对时间的请求抽象到一个你能够控制的类中。
你的系统可能需要在每天晚上八点触发一个处理过程,也可能在启动下一步前要等上500毫秒,也可能要在每个闰年的二月二十九号做一些特殊的处理。
通常,我们使用依赖注入把用到的系统时间行为注入到包装类中(wrapper)。通过这种方法,我们就可以为Clock这个类的行为进行打桩或模拟,或做一些我们认为合理的抽象。在我们的测试中,如果我们能设定当前是闰年,或要延时500毫秒的话,那么它就完全在我们的控制之下了。
只要代码中需要使用时间,我们就会抽象对系统时间服务的请求,而不是直接在业务逻辑中调用它们。
蛮力
开发人员总是为最快的提交周期争论不休。然而,事实上,这要与在提交阶段识别最常见错误的能力平衡考虑。这是个只能通过不断试错才能找到的优化过程。
有时候,运行速度稍慢一点儿的提交测试可能优于通过优化测试或减少发现的缺陷数来追求运行速度的提交测试。
小结
提交测试应该聚焦于一点,即尽快地捕获那些因修改向系统中引入的最常见错误,并通知开发人员,以便他们能快速修复它们。提交阶段提供反馈的价值在于,对它的投入可以让系统高效且更快地工作。
提交阶段的创建(一个每次修改都会触发的自动化过程,它将构建二进制包、运行自动化测试,并生成有效的度量报告)是采纳持续集成实践的一个最小集。
假如你遵循了由持续集成引入的其他实践,比如定期提交,以及一旦发现缺陷就尽快修复,那么提交阶段会让交付流程在质量和可靠性方面有相当大的进步。尽管它只是部署流水线的起点,但可以为你提供巨大的价值,比如可以马上知道谁在什么时候提交的修改让应用程序无法工作,并能够马上修复,令应用程序恢复工作。