7 测试

微服务应用在建造的时候应该考虑到测试。好的测试能够让代码更加友好,正向影响持续提交流程。

根据应用组件的生命周期,可以分为以下几类测试:

  1. 单服务测试
    由分离的团队实行测试
  2. 临时环境
    在临时环境执行测试,微服务组成一个特别的应用部署到一个临时环境中,用于测试。
  3. 生产环境
    运行在生产环境的测试

测试应该作为构建(build)、发行版(release)、运行(run)流程中自动执行的部分。

7.1 测试类型

在微服务中包含以下几类测试:

  1. 单元测试
    单元测试是一个简单的类或者耦合相邻的类集合。这些单元测出可以使用实际的对象运行或者部署后使用test doubles 或 mocks。
  2. 组件测试
    测试一个微服务全部的功能。在测试过程中,通过mock的方式调用外部服务。
  3. 集成测试
    集成测试用于测试服务之间的通信,用于测试一个网络边界中最基本的成功或错误路径。
  4. 契约测试(Contract)
    测试微服务中API和其他资源的契约程度。
  5. 端到端测试
    测试微服务或应用中全部流程。通常用于测试一个全路径或校验应用,是否满足扩展的需求。

7.2 应用架构

微服务内部结构直接影响到测试的容易程度。如下图所示,一个简单的微服务内部结构。

测试为什么要懂微服务架构 微服务软件测试_测试为什么要懂微服务架构

不同的部分需要不同的测试,例如,domain 逻辑层不需要集成测试,但是resource需要。分离的代码,导致微服务中相同部分需要相同的测试需求。这种技术使得测试套件简单并且更好的结构。由于微服务架构天生的复杂性,测试很容易别的复杂,因此,具有结构化的测试将有利于测试的开展。

7.3 单服务测试

单服务测试定义为独立服务拥有者需要创建和运行自己的测试。这些测试运行的代码是包含在逻辑边界中(logical boundary,如上图所示)。三种测试将应用在单服务中: 单元测试、组件测试、集成测试。单元测试和集成测试用于测试一个微服务中独立的部分,组件测试将测试整个服务。

测试业务功能

微服务中的业务代码不应该调用应用外部的服务,这些代码可以使用单元测试进行测试,例如JUnit。单元测试应该测试任何操作中实际使用的或mock的对象的行为。

测试Resources

提供资源的端点或接收事件的端点需要进行测试:集成测试(integration)或契约测试(contract test)

集成测试
集成测试应该用于检测跨网络的边界之间的通信。他们应该测试基础的成功或失败路径。集成测试可以采用与单元测出类似的方式进行,或与运行服务器一起。为了不需要启动服务器来运行集成测试,可以直接调用JAX-RS注解的方法。在测试过程中,创建mock的对象或资源类,当调用资源服务时。

集成测试应该验证应用基本的成功或错误路径。不正确的请求应该返回有用的响应,以及合适的错误码。

消费者驱动契约(Consumer driven contract)
一个应用中的消费者拥有一系列的输入和输出属性,需要对应的服务。这些输入和输出包括数据结构、执行和会话。契约可以使用Swagger这类工具,进行文档化。

消费者驱动契约测试是一个测试集合,用于决定这个契约是否支持。这些测试应该验证在契约中定义好的预期输入属性,同时也应该接受未知的属性,要验证资源服务返回文档中定义的属性。

维持消费者驱动契约测试会引入组织复杂性,如果测试不能精确的测试定义的契约,那么这些测试将是无用的。此外,如果契约过时了,即便是最好的测试也不能产生有用的资源。因此,消费者驱动契约测试需要保持更新状态,与当前消费者的需要同步,这样才能确保测试精确。

契约测试需要实际的API。可以使用Swagger editor来创建测试。测试运行在服务器端。

tests on running server
启动和停止服务作为自动化测试的一部分,有一些方法可以实现。例如Maven和Grade的插件。例如 WebSphere Application Server Liberty
Maven https://github.com/WASdev/ci.maven
Grade https://github.com/WASdev/ci.gradle

另一个契约测试维度,契约测试运行在消费者端。这些测试应该运行在消费者可以访问运行版的服务环境中,stagin environment。具体可以参阅 7.4.3节。

测试外部服务请求

不可避免地,微服务需要调用外部服务来完成一些请求,例如调用应用之外其他微服务或服务。提供这些功能的类,构建一个客户端来发起请求并处理失败的请求。测试外部服务的请求,可以使用两类集成测试方法:一个是在单服务级别,一个是在staging 环境下。两种测试方法都测试客户端基本的成功和错误处理功能。更多可以参阅7.4.2.

单服务层级的集成测试不需要服务在测试下,或外部服务被部署。为了执行集成测试,需要伪造外部的服务进行响应。如果使用JAX-RS2.0 客户端调用外部请求,可以使用JMockit框架进行测试。

测试数据请求

在微服务架构中,每个微服务有自己的数据。如果根据这条开发指南,一个微服务的开发者可能负责一个外部数据存储(data store)的使用。如7.2节,应用架构图所示,执行外部数据的映射和校验包含在repository层。当我们测试domain逻辑层时,这个层需要进行伪造。测试数据请求,微服务中集成测试用于测试数据映射或验证,需要在本地或私有云中部署一个测试的数据存储。测试数据请求的基础的成功或失败路径。如果数据映射或验证需要扩展测试,可以尝试将这部分代码分离出来,使用伪造的数据客户端进行测试。

测试数据
本地版本的数据存储必须包含测出数据,并且思考哪些数据需要放到数据存储中。这些数据应该与生产环境中的数据结构一样,没必要太复杂,应该可以满足指定目标的数据请求测试。

组件测试(component testing)

组件测试用于测试每个微服务。组件是网络访问中任何一块,因此调用外部服务可以使用伪造或替代的“test-service”进行测试。两种方式各有优劣。

使用mocks
伪造调用外部服务的请求,需要配置很少的测试对象。如果能够很容易地定义伪造系统的行为,例如使用JMockit,没有测试会应为网络问题而失败。这种方法的弊端是,这些并没有完全使用组件,因为我们截断了一些调用,增加了这里面的bug风险。

Test Service
为了使用微服务完整的通信边界,我们创建一个测试服务,来模仿生产环境中调用的外部服务。这种测试服务可以包括数据测试。测试服务也可以当做微服务的消费者使用。弊端是,这种方式需要我们维护测试服务,也需要更多循环处理,需要更完整的测试来测试微服务并创建一个部署的管线。

当我们使用mock框架进行其他级别的测试,重复使用这种技能也是有意义的。然而,如果我们使用mocking方法,必须确保这些staging环境下的测试能有效使用服务内部的通信。

安全检验

在服务式系统中安全非常重要,我们不在将应用放到防火墙后面,并假设不会有东西打破这层防护。

根据我们实现安全方式的不同,微服务中的安全测试稍微有所不同。如果每个独立的服务都做token验证,那么安全测试在这个服务级别。如果使用其他服务或库来验证tokens,那么这个服务需要独立测试,并且staging环境下的交互也需要测试。

7.4 临时环境(Staging Environment)

这部分定义一个临时环境作为测试环境,与生产环境尽可能相同。组建管线部署成功地测试微服务架构至临时环境,验证微服务中跨逻辑边界的通信。

测试数据

临时环境应该包括生产系统中任何类型的数据存储。这些存储中的数据比每个服务层级的数据更完整,要测试更加复杂的交互。

使用工具注入数据来测试。这些工具可以能提供系统中数据流的控制,测试当有坏数据引入发生的问题。

集成测试

集成测试用于测试系统中所有服务之间的交互。在这个阶段测试每个独立服务深度的行为。消费者驱动契约测试应该确保服务之间成功地交互,而不是仅仅定义失去的bugs。需要测试服务通信之间基本的成功和失败路径。

在测试工程中可能需要伪造某些服务,而不是一次测试所有的服务。测试两个指定服务之间的交互,或一部分微服务之间交互,当调用这部分微服务之外时,添加伪造的行为。伪造服务外部的调用与API的单元测试采用一样的方式。相同的技术来进行启动、停止服务器或容器。

契约

每个消费其他服务或资源的服务,应该拥有一部分契约测试,这些测试运行在资源(特别是临时环境)之上。为了让服务能独立的演进,很重要的一点是确保消费者契约能持续满足要求。

这些测试由消费者编写(客户端),作为消费服务器测试工具中运行或管理的一部分。相反,资源测试由提供者信息编写,并验证整个契约。

端到端

端到端测试是用于查找之前缺失的bug,端到端测试应该使用应用中合适的“黄金路径”(golden paths)。用端到端测试整个应用中的路径是不现实的,因此识别主要的路径进行测试。有一个很好的方法识别这些路径,预览应用主要的外部需求。例如,一个在线应用零售店的系统,可能需要测试这些路径:

  1. 用户登录
  2. 用户购买一个商品
  3. 用户浏览订单信息
  4. 用户取消订单

端到端测试应该包含用户界面,例如 SeleniumHQ 可以自动测试Web交互。

容错和弹性

一个微服务应该包含容错设计,因此我们需要测试微服务中的容错和弹性功能,容错测试应该在所有服务部署下进行,而不是使用mock。

拆卸微服务
在一个微服务系统中,我们不能依赖所有的微服务在任何时候都可用,在测试阶段,当拆卸、重复部署一个服务时发起请求。这个过程应该包括微服务以及后端的数据存储。监控请求返回的时间,识别一个可接受响应的时间。如果响应时间太长,考虑到减少timeout的时间或警告熔断配置。

有很多自动化工具可以帮助测试,例如,Java社区非常有名的Netflix Chaos Monkey,可以终端一个虚拟机的实例来进行容错测测试。https://github.com/Netflix/SimianArmy/wiki/Chaos-Monkey

另一个工具,例如 Amalgam8中使用的 Gremlin, 截断和手动调用微服务,而不是停止实例。
https://github.com/ResilienceTesting/gremlinsdk-python

注入坏数据
正如集成测试中,像Amalgam8可以用于注入坏的数据,进行测试。如果微服务中使用了隔板模式,当有一个微服务中有坏的数据时,不会传递到其他微服务。

压力测试
微服务应该能够处理异常的负载,压力测试应该测试微服务中的隔板,如果一个实际的微服务能够顶住请求,可以查看一下bulk head配置。

可恢复性测试(recoverability testing)
一个微服务应该具有弹性能力,如果容器或虚拟机宕机,微服务应该能够快速恢复。编排工具,例如 kubernetes会启动新的实例,如果应用宕机。一个微服务应该快速启动,当一个服务需要停机时,应该能平稳的停机。

使用实例拆卸的方式进行恢复性测试。使用编排工具创建新的实例,并监控重启一个新的服务需要花费多长时间。

7.5 生产环境(Production Environment)

为了测出系统,我们应该考虑生产环境下的运行情况。由于很多变迁的部分以及服务持续的添加或移除,我们不应该期待系统能够继续完美而不确定性的工作。添加监控和分析应用来警告任何坏的行为是非常重要的。

生产环境也需要开展临时环境下的测出:

  1. 注入坏数据测试
  2. 宕机测试,测试服务的容错性和恢复性
  3. 安全验证

可以使用与临时测试环境一样的工具,但是我们需要注意这些运行这些测试,不要在高峰值时,运行这些测试。选择一个时间集合,对客户端应用影响很小的时候运行这些测试。

综合监控(synthetic monitoring)

为了检测生产系统的健康状态,创建综合事务贯穿整个应用。使用浏览器仿真器生成综合事务和监控响应。查看无效的响应或大范围响应时间。周期性的自动构建系统。如果提交经常提交新的代码,构建将频繁运行,但是很可能实际的服务可能需要一段时间,当有少量的提交时。在这段时间内,如果继续运行构建,将发现任何应用依赖的改变可能导致问题。

金丝雀测试(canary testing)

金丝雀测试名称由来于矿洞前期入洞前检测矿洞内环境是否对矿工安全。最小的测试用于验证一个服务的所有依赖和基础的操作需求功能。如果环境是不安全的,金丝雀测试将快速失败,提供一个较早失败的指标。