在微服务架构下,单个微服务的测试确实变得更加简单,但是与此同时,模块(微服务)与模块(微服务)之间的测试变得困难起来。
这是因为所有的微服务都可能处于同一局域网的不同服务器上,想要对他们进行测试并不是一件容易的事情。
进程间通信是微服务架构的核心,基于微服务架构的应用程序是一个分布式系统。
在开始回答这个问题之前,我们先来了解下什么是测试?
测试用例是用于特定目标的一组测试输入,执行条件和预期结果,例如执行特定的程序路径或验证是否符合特定要求。——维基百科
也就是说,测试就是输入一些测试数据,程序执行后得到我们期望的结果,如果一致,测试通过,反之有问题。
知道什么是测试之后,我们再来了解下都有哪些测试类型。
因为我们必须为我们的应用程序编写不同类型的测试,来确保应用程序有效。
根据测试范围的不同,大致可以将测试分为如下四类:
- 单元测试:在java这样面向对象语言中,测试的目标就是类,一般由开发人员完成。
- 集成测试:验证服务是否可以与基础设置服务如数据库,消息中间件或其他应用程序服务进行交互。
- 组件测试:单个微服务(模块)的验收测试
- 端到端测试:整个应用程序的验收测试(各个微服务之间的调用测试)
这四种测试的难易程度不尽相同,一个极端是单元测试,执行起来快,易于编写且可靠。
另一个极端是针对整个应用程序的端到端测试,也就是微服务到微服务之间的测试。
下面这张测试金字塔图挺不错的,很好总结了各个测试类型的难易程度。
PS:另一个参考价值在于,测试的过程中, 多点单元测试,集成测试次数,相对少点组件测试和端到端测试次数。
JUnit 是一种流行的Java 测试框架,每个测试都有一个测试方法实现。
一个测试类最好有一个在所有测试类执行前的初始化方法,以及在最后运行的清理方法。
- 在JUnit5 有了很多新的注解可以帮助我们高效完成单元测试需求
- 比如我们可以使用
@BeforeAll
可以在当前类的所有方法之前前执行方法,@AfterAll
在所有测试方法执行后执行方法,- 甚至可以使用
@TestMethodOrder
注解指定测试方法的执行前后顺序。- 点击查看更多注解
如果我们想只是测试一个Controller 而把整个应用程序所有组件都启动起来是不切实际的。
因此,针对单元测试的一个建议是尽可能使用测试替身。
测试替身一般有两种,一种是使用Mock对象,另外一种是Stub对象。
关于Mock 对象,很多人应该比较熟悉,MockEnvironment
,MockMvc
,
这里贴一个例子:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
@Test
void getAccount() throws Exception {
this.mockMvc.perform(get("/accounts/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$.name").value("Lee"));
}
}
- 更多其他Mock对象框架选择: Mocketito 是一种流行的Java模拟对象框架
- 更多学习和用法请参考
- Spring MVC Test
- Spring Boot Test Features
- Spring Boot test Auto Configuration
另外一种Stub (存根)对象,这个是神马东东呢?
Stub 江湖俗称占坑对象,稍后详解。
为屏蔽客户调用远程主机上的对象,必须提供某种方式来模拟本地对象,这种本地对象称为存根(stub),存根负责接收本地方法调用,并将它们委派给各自的具体实现对象
假设我们有一个包含多个微服务的系统,如下图所示:
如果我们要测试上图中这个应用程序,看看它是否可以与其他服务通信,我们可以执行以下两项操作之一:
这两种方法都有优点,也有很多缺点。
第一种方式:部署所有微服务并执行端到端测试
优点:
- 模拟生产环境
- 测试服务之间的真实通信
缺点:
- 为了测试一个微服务,我们必须部署六个微服务,几个数据库以及其他项目
- 测试运行的环境被锁定为单个测试套件(在此期间,其他任何人都无法运行测试)
- 需要很长时间才能运行
- 反馈在此过程中非常慢
- 很难调试
第二种方式:在单元和集成测试中模拟其他微服务
优点:
- 他们提供了非常快速的反馈。
- 他们没有基础架构要求。
缺点:
- 服务的实现者创建的存根可能与现实无关。
- 您可以通过测试并通过失败的生产。
因此最好避免像这样的端到端测试。
那么有没有什么更好的测试方法呢?
答案是肯定的,那就是使用契约测试。
- 契约测试通常使用样例测试,消费者和提供者之间的交互由一组样例定义,称为契约。
- 每个契约都包含在一次交互期间交换的样例消息。
契约测试提供了一种新的测试方式 - 基于接口,也就是说每个微服务都提供自己的测试接口以及请求的测试数据,期待的测试结果。
例如,REST API 的契约包含示例HTTP 请求和响应。
契约测试的主要设计思想是为我们提供非常快速的反馈,而无需建立整个微服务。 如果我们使用stub,则仅需要应用程序直接使用的应用程序。
可以把契约理解成接口,不过这个接口不是传统意义上的接口。它是一段groovy或yaml代码,消费者和生产者在这段代码中约定了request 什么就可以response什么。一方面生产者可以自己去验证是否实现了契约,即真实开发完成后检查能否实现request什么,就返回什么的契约。消费者测试不会真的等生产者启动服务,而是使用框架给生产者根据契约生成一个模拟的stub(mock)文件,它可以提供同样的模拟服务,比如request什么,response什么。
两个比较流行的企业级契约测试框架是Spring Cloud Contract 和支持多种语言的Pact系列框架。
默认情况下,Spring Cloud Contract与Wiremock集成为HTTP服务器Stub(存根)。
这里贴一段示例体会下,更多请移步官方文档参考学习。
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
// (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
"client.id": $(regex('[0-9]{10}')),
loanAmount : 99999
])
headers {
// (5)
contentType('application/json')
}
}
response {
// (6)
status OK() // (7)
body([ // (8)
fraudCheckStatus : "FRAUD",
"rejection.reason": "Amount too high"
])
headers {
// (9)
contentType('application/json')
}
}
}
其实很多企业都是手动测试,缺乏自动化测试的原因主要是文化,比如“测试是QA的工作,开发人员的时间不应该花在测试上”,还有一部分原因是一些开发人员并不十分熟悉如何写好测试用例。
但是在微服务架构下最好的实践是集成自动化测试。
上图所示的部署流水线包括如下阶段:
- 提交前的测试阶段:执行单元测试,这是由开发人员在提交代码变更之前执行的。
- 提交测试阶段:编译服务,执行单元测试,并执行静态代码分析。
- 集成测试框架:执行集成测试
- 组件测试阶段:执行服务的组件测试。
- 部署阶段:将服务部署到生产环境中。
当开发人员提交代码更改时,持续集成服务器(比如Jenkins)就会运行提交测试。
处理提交前的测试不是自动化的,其他部分基本上都是自动化测试。
但是值得注意的是,有些情况仍然需要手动操作,比如将项目打包发布到生产环境的时候。
在这种情况下,最好只有测试人员单击一个按钮表明该阶段通过时才会进入下一阶段。
单体架构下应用开发的测试通常都是在开发完成后执行,而且大多都是手工测试,但是当到了微服务架构这种方式不再适用。
总结建议如下:
- 自动化测试是快速,安全交付软件的重要基础,由于微服务架构固有的复杂性,必须实现自动化测试。
- 简单和加快测试的方式是使用测试替身,比如Mock 对象或Spring Cloud Contract.
- 根据测试金字塔参考应该尽可能减少端到端测试的数量,因为写入耗时,脆弱,且耗时。
本篇完~