《持续交付 发布可靠软件的系统方法》读书笔记
验收测试在部署流水线中是一个关键阶段:它让交付团队超越了基本的持续集成。一旦正确实施自动化验收测试,你就是在测试应用程序的业务验收条件,即验证应用程序是否为用户提供了有价值的功能。验收测试通常是在每个已通过提交测试的软件版本上执行的。验收测试与功能测试或单元测试有什么不同呢?对于一个单独的验收测试,它的目的是验证一个用户故事或需求的验收条件是否被满足。验收条件有多种类型,如功能性验收条件和非功能性验收条件。非功能性验收条件包括容量(capacity)、性能(performance)、可修改性(modifiability)、可用性(availability)、安全性(security)、易用性(usability),等等。其中的关键点在于,当与某个具体用户故事或需求相关的验收测试成功后,就表明这个用户故事或需求已满足验收条件,可以认为它已完成并且是可正常工作的。验收测试是针对业务的,而不是面向开发的。
为什么验收测试是至关重要的
通过合理地创建和维护自动验收测试套件,其成本就会远低于频繁执行手工验收和回归测试的成本,或者低于发布低质量软件带来的成本。我们还发现,自动化验收测试能捕获那些即使单元或组件测试特别全面也都无法捕获的一些问题。除验收测试外,没有哪种测试能够基本上代替生产环境中的实际运行来证明软件能为客户提供他们所期望的业务价值。单元测试和组件测试都不测试用户场景,因此也无法发现那种用户与应用程序进行一系列交互后呈现出来的缺陷。而验收测试就是为这而设计的。
如何创建可维护的验收测试套件
要写出可维护的验收测试套件,首先需要细心地关注分析过程。验收测试来源于验收条件,因此写应用程序的验收条件时必须想着如何使其自动化,并要遵循 INVEST 原则,尤其是“对最终用户有价值”和“可测试”这两点。INVEST 原则指独立性(independent)、可协商的(negotiable)、有价值的(valuable)、可估计的(estimable)、小的(small)和可测试的(testable)。一旦你拿到了一些验收条件来描述对用户的价值,下一步就是将它们自动化。
GUI 上的测试
在写验收测试时,一个非常重要的考虑是:测试是否直接基于应用程序的 GUI 运行。在应用程序的开发过程中,用户界面通常会频繁变化。如果验收测试与 UI 耦合,那么 UI 的微小变化很容易就能破坏验收测试套件。假如应用程序设计得比较好,GUI 层仅是清晰定义用于数据展现的代码,不包括任何业务逻辑。在这种情况下,绕过界面,基于界面下的代码进行测试的风险会相对小一些。如果应用程序能够做到这一点的话,我们建议直接基于业务层执行测试,这是一个合理的策略。唯一的要求就是开发团队在这方面的纪律性,即让表现层只负责展现,不要涉足业务领域或应用逻辑。
创建验收测试
分析人员和测试人员的角色
建议大多数项目(无论大小)都应该有一个业务分析师作为核心团队的一部分,与团队一同工作。业务分析师这个角色主要代表客户和系统的用户。他们与客户一起工作,识别需求,并排定优先级。他们与开发人员一起工作,确保开发人员能从用户的角度很好地理解应用程序。他们对开发人员进行指导,确保那些用户故事真正交付了它们应有的业务价值。他们与测试人员一起工作,确保验收条件已被合理阐明,并且开发出来的功能满足这些验收条件,交付了期望的价值。任何项目中,测试人员都是至关重要的。他们的角色就是确保交付团队的每个人(包括客户)都了解并理解正在开发的软件的当前质量和生产准备情况。为了做到这一点,他们要与客户和业务分析师一起工作,为用户故事或需求定义验收条件,与开发人员一起工作,编写自动化验收测试,他们还要执行手工测试活动,比如探索性测试、手工验收测试和演示。
迭代开发项目中的分析工作
在迭代交付方法中,分析人员会花大量时间定义验收条件。团队用这些验收条件来评判某个具体需求是否被满足。一旦验收条件定义完成,在开始实现这个需求之前,分析人员、测试人员应该和将要实现这个需求的开发人员碰一下头儿(有客户在就更好了)。分析人员讲解需求,以及它的业务上下文,并检查一遍验收条件。然后,测试人员和开发人员讨论,并就“实现哪些自动化验收测试来证明验收条件被满足”达成一致。在实现需求时,如果开发人员发现对某个地方不是非常理解(或者发现了一个问题,或找到更高效的方法可以解决需求问题),就可以去问分析人员。当开发人员认为工作已经完成时,通常是指所有相关的单元测试和组件测试都已经通过了,验收测试也全部实现,并证明系统满足需求。此时,他们就可以向分析人员、测试人员和客户进行演示。一旦分析人员和客户对需求很满意,就可以交给测试人员进行测试了。
将验收条件变成可执行的规格说明书
对那些使用迭代过程的项目来说,由于自动化测试变得更加重要,所以,很多实践者都认识到,自动化测试不仅仅是测试而已。相反,验收测试就是正在开发的应用程序行为的一个可执行规格说明书。验收测试是面向业务的,所以它们应该验证应用程序的确向用户交付了价值。分析人员为用户故事定义验收条件,只有这些验收条件被满足了,这个用户故事才算完成。
应用程序驱动层
应用程序驱动层是一个知道如何与应用程序(即被测试的系统)打交道的层次。应用程序驱动层所用的API是以某种领域语言表达的,可以认为是一种针对它自己的领域专属语言。什么是领域专属语言?DSL(Domain-Specific Language,领域专属语言)是一种计算机编程语言,用于解决某个具体问题域的某个问题。它与通用编程语言不同,因为它无法像通用编程语言那样可以解决很多类型的问题,它专门为解决某个专属问题域的问题而设计。
如何表述验收条件
如果分析人员和客户有足够的技术背景,能够使用内部DSL编写的xUnit测试的话,直接使用xUnit这种方法最好。它不太需要那些复杂的工具,只要会使用开发环境中的自动完成功能就可以了。
窗口驱动器模式:让测试与 GUI 解耦
窗口驱动器模式是通过提供一个抽象层,减少验收测试和被测试系统GUI之间的耦合,从而让基于GUI的测试运行时更加健壮。它有助于隔离系统GUI的修改对测试的影响。实际上是写了一个抽象层,作为测试的用户接口。所有测试都要通过这个抽象层与真正的UI进行交互。所以,如果对GUI做了一些修改,我们可以对窗口驱动器做相应的修改,这样就不用改测试了。
实现验收测试
验收测试的实现当然不仅仅是分层问题。它还包括让应用程序达到某种特定状态,然后再执行几个操作,之后再验证结果。另外,它还要能处理异步问题和超时问题。测试数据也要细心管理。还常常需要使用测试替身,以便模拟与外部系统的集成。
验收测试中的状态
验收测试要模拟真实的用户在真实的应用场景下与系统进行交互,并验证系统功能是否满足业务需求。当用户与系统进行交互时,他们会建立并依赖于系统中所管理的状态信息。如果没有这种状况信息的话,验收测试就没有意义了。但建立一个已知的起始状态,准备真正测试的条件,然后在这个状态下运行测试是很困难的事情。当你发现必须创建一个无法保证初始状态而且运行后也无法清理干净的测试时,建议你集中精力,让这样的测试有绝对的防御性。在测试开始之前验证其状态是否符合你的期望,如果有任何异常之处,就马上让这个测试失败。
过程边界、封装和测试
大多数情况下,你应该怀着下面这种愿望来写测试代码,即这些代码的存在只是为了验证应用程序的行为。努力避免这种受限访问,为自己设定一个底线,努力思考,直至你非常肯定自己已无法再找到更好的方法之前,决不放弃。
管理异步与超时问题
异步系统的测试有其独特之处。就单元测试来说,在单个测试范围之内,应该避免所有异步情况,也要避免跨越测试边界的情况。对于验收测试来说,根据应用程序本身的特点,异步可能是不可避免的。是让这个测试失败呢?还是一直等待,直到返回结果?我们发现,最有效的策略是构建一个夹具用于将测试本身与这个问题隔离开来。诀窍是,对于测试本身而言,让事件顺序发生,使测试看起来像是同步的。这可以通过把同步调用背后的异步性隔离开来实现。
使用测试替身对象
在做验收测试时,应该最小化外部依赖的影响。我们通常创建测试替身对象,用于代表系统与所有外部系统交互的连接器。在测试中,用替身对象取代外部系统还有一个好处,那就是能够控制行为、模拟通信失败、模拟错误响应事件或高负载下的响应等,所有这些都能在我们的掌握之中。应用一些好的设计原则,可以让外部系统与你开发的系统的耦合最小。通常会设计一个系统组件专门与某个外部系统进行交互,也就是说,每个外部系统对应一个内部组件。该组件把这些交互及与其相关的问题集中到一点,并将这些交互的技术细节与系统的其他部分隔离开来。
验收测试阶段
确保验收测试一直处于通过状态
由于运行高效的验收测试套件的时间问题,它通常运行在部署流水线中比较靠后的位置。这么做引起的一个问题是,如果开发人员没有像等待提交测试那样,坐在那里等着这些测试运行通过的话,那么他们常常会忽视验收测试的失败。对于部署流水线来说,这种低效性是我们能够接受的妥协,因为这样能在提交测试阶段快速捕获大多数失败,并且也维持了比较高的自动化测试覆盖率。但这也是一种反模式。说到底这是一个纪律问题,整个交付团队应该为保持验收测试通过负责。
部署测试
当运行验收测试时,我们设计的测试环境会尽可能与期望的生产环境一致。如果成本不太高的话,它们就应该是一样的。否则,尽可能利用虚拟技术来模拟生产环境。所用的操作系统和任何中间件都应该和生产环境一致,在开发环境中已经模拟或者被忽略的那些重要的流程边界一定会在这里出现。这就意味着,除了测试是否满足验收条件以外,这还是验证类生产环境自动化部署和部署策略是否能够工作的最早时刻。我们常常选择运行一小撮冒烟测试,用于断言我们配置的环境与期望一致,并且系统中各种组件中的通信也是正常的。我们有时把这叫做基础设施测试或环境测试。但实际上,它们是部署测试,目的在于证明部署非常成功,并为更多功能验收测试的执行建立完好的起始状态。
验收测试的性能
重构通用任务
最显而易见且快速奏效的方法就是每次构建结束后都找到最慢的几个测试,再花上一点儿时间找些办法让它们更加高效。这种策略与我们管理单元测试的方法相同。无论采用什么样的机制,对测试进行重构,通过创建测试辅助类,确保测试在执行通用任务时所用的代码相同,这对更好的测试性能和更高的可靠性是非常重要的步骤。
共享昂贵资源
我们要找出测试间会共享哪些资源,以及哪些资源要被单个测试独占。通常,对于大多数基于服务器的应用程序来说,都可以共享这个服务器的同一个实例。在执行验收测试前,创建一个干净的系统运行实例用于测试,在这个实例上运行所有的验收测试,最后再将它关闭。根据被测系统的特质,有时候可对其他的耗时资源进行优化,使验收测试套件在整体上能更快地执行。
并行测试
当验收测试间的独立性比较好时,还有一种办法可加速测试的运行,那就是“并行执行测试”。对于那些基于服务器的多用户系统来说,这是显而易见的。如果你能将测试分开,并且保证它们之间没有互相影响的话,那么,在同一个系统实例上并行执行测试会大大减少验收测试阶段运行的总时长。
使用计算网格
对于那些非多用户系统,或者那些极其昂贵的测试,或者那些需要模拟并发用户的测试来说,使用计算网格的益处非常大。当与虚拟服务器结合使用时,这种方法就变得极其灵活且可扩展了。你甚至能让每个测试运行在属于它自己的虚拟机器上。这样,验收测试套件的时间再长,也就是那个运行得最慢的测试所用的时间了。
小结
使用验收测试对提高开发流程的效率非常重要。它使交付团队的所有成员都关注于真正的工作:用户想从应用程序中得到什么。自动化验收测试通常要比单元测试复杂,需要更多的时间进行维护。而且,由于它在修复某个失败与使所有验收测试套件成功通过之间那种固有的滞后性,所以与单元测试相比,它处于失败状态的时间要长一些。然而,如果把它作为从用户角度看待系统行为的一种保障的话,它为复杂的应用程序在整个生命周期中的回归问题提供了一个良好的防范性。我们认为,“拒绝未能通过验收测试的候选发布版本”这一纪律是交付团队开发高质量软件过程中前进的一大步。近年来,越来越多的团队开始关注并使用单元测试。与只依赖手工测试相比,已经算是向前发展了一大步。然而,根据我们的经验,它仍然会导致代码没有做用户想要的东西,因为单元测试的关注点并不是业务本身。我们相信,采纳验收测试条件驱动的测试代表了更先进的理念,因为它:
- 为“软件是否满足业务目标”提供了更高的信心;
- 为系统进行大范围修改提供了一个保护网;
- 通过全面的自动回归测试极大地提高了质量;
- 无论什么时候出现缺陷,都能提供快速、可靠的反馈,以便可以立即修复;
- 让测试人员有更多的时间和精力去思考测试策略、开发可执行的规格说明,以及执行探索性测试和易用性测试;
- 缩短周期时间,使持续部署成为可能;