在微服务体系中,开发者要进行接口测试,一般有以下几种方法:
1. 搭建完整的微服务环境,将所有依赖的微服务全部运行起来,然后针对要测试的微服务写测试用例;
2. 使用 Mock 来模拟依赖的微服务以及数据库的读写;
3. 契约测试,服务的提供者和消费者按照同样的契约编写自己的测试用例。
这其中,方法1的工作量比较大,维护这么一个环境也是一个麻烦的事情,但是能真实模拟请求的完整流程;方法2能让测试集中于自己的微服务中,但是一旦依赖的接口有变化,Mock并不能及时的反映出来,要到集成测试的时候才可能发现,这是个隐患;方法3在微服务架构中是一个比较好的方法,服务的提供者和消费者同时按照同一个版本的契约进行各自独立的开发和测试,又不用完整的运行整套微服务体系,在便捷性和准确性上都有一定的保证。
本文介绍在 Spring Cloud 微服务中,如何优雅的编写接口测试用例,这其中依赖到了 Spring Cloud Contract(契约测试框架),DbUnit(数据库工具,用来模拟数据库的读写)。一个好的测试用例,应该在测试接口逻辑的完整性的条件下,不会对数据库造成破坏(这就要使用DbUnit工具),运行测试用例时不会依赖其他的微服务(这就要使用契约测试)。
首先介绍下示例项目依赖的版本:
Spring Cloud: Greenwich.RELEASE
DbUnit: 2.6.0
spring-test-dbunit-core: 5.2.0 (注意这个组件不能用 https://github.com/springtestdbunit/spring-test-dbunit 这里面的,这个是比较旧的版本,已经无人维护,Spring boot 1.X 可以使用,Spring Boot2 就不行了,需要用 https://github.com/ppodgorsek/spring-test-dbunit 这个,这是对旧项目的 fork ,进行长期维护的版本)
具体的依赖还需要根据实际的 Spring Cloud 版本进行更换。
一、使用 DbUnit 完成对数据库层面的Mock
DnUnit工具具体使用方法请自行百度,它的实现逻辑是根据你提供的数据库连接信息,将对应的数据库进行备份,然后将你准备的测试数据写入到数据库中,之后执行测试用例,所有测试用例执行完毕之后,再将备份信息还原到数据库中,这样就避免了对数据库的破坏。
首先准备测试数据,在 src/test/resources 下面建立 testData.xml 文件,按照如下格式写入测试数据
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user_ user_uuid="11111111" account="zhangsan" user_name="张三"/>
<user_ user_uuid="22222222" account="lisi" user_name="李四"/>
</dataset>
假设我们有一个接口 http://localhost:8080/user/${userUuid} 根据 userUuid 获取用户信息,具体的实现不列出了,这不是这篇文章的重点,我们只要有这个接口存在就行,它会返回如下格式的json数据
{
"errorCode": 0,
"errorMsg": "SUCCESS",
"data": {
"userUuid": "11111111",
"account": "zhangsan",
"userName": "张三"
}
}
然后编写测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional(transactionManager = "transactionManager")
@Rollback(value = true)
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionDbUnitTestExecutionListener.class,
DbUnitTestExecutionListener.class })
@DatabaseSetup("/testData.xml")
public class UserControllerTest {
private static final Logger LOGGER = LoggerFactory.getLogger(UserControllerTest.class);
private ObjectMapper mapper;
@Autowired
public MockMvc mvc;
@Before
public void setUp() {
LOGGER.info("UserControllerTest init");
RestAssuredMockMvc.mockMvc(mvc);
this.mapper = new ObjectMapper();
}
@Test
public void testCreateUser() throws Exception {
this.mvc.perform(MockMvcRequestBuilders.get("/user/11111111")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("errorCode").value(0))
.andReturn();
}
}
编译运行,测试用例通过,可以查看下实际的数据库是不是还是原来的状态,如果是则表示 DbUnit 工具引入成功。当然这过程中编写类似创建用户的测试用例,更能看出来的 DbUnit 是否生效。
这中间过程中你可能会碰到一个问题: org.dbunit.database.AmbiguousTableNameException: EVALUATE ,这是一个很坑的问题,我在这个问题上纠结了两天,各种百度 google 无果,最后发现是 Spring Cloud Greenwich.RELEASE 版本使用的 mysql-connector-java 是 8.0的版本,需要将其改成 5.X的版本才能使得 DbUnit 正常运行。
DbUnit 完美运行之后,接下来就是契约测试了。
二、Spring Cloud Contract
具体如何使用请自学。
先说下契约这个东西,对于服务提供者而言,契约可以用来约束其单元测试用例,服务提供者编写的测试用例,必须符合这个契约,才能保证服务提供者提供的接口确实是符合这个契约的。对于服务消费者而言,契约可以模拟其调用这个微服务时,会得到什么样的结果。编写契约可以使用 groovy 或者 yml,Spring Cloud Contract 可以根据这个契约生成 测试用例,我们可以有效利用这一点,简化服务提供者的单元测试用例的编写工作。
服务提供者:
引入依赖
<dependencies>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
...
</dependencies>
<build>
<plugins>
...
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>2.1.0.RELEASE</version>
<!--Don't forget about this value !!-->
<extensions>true</extensions>
<configuration>
<!--MvcMockTest为生成本地测试案例的基类-->
<baseClassForTests>com.walli.user.service.test.UserControllerTest</baseClassForTests>
</configuration>
</plugin>
...
</plugins>
</build>
这里说明下 baseClassForTests 这个属性配置,这里声明 Spring Cloud Contract 自动生成测试用例的时候的基类,在这个基类中,你需要注入 MockMvc 的上下文(上面的测试类示例代码中的@Before RestAssuredMockMvc.mockMvc(mvc) 这一行)。
然后编写契约,我采用的是 yml 方式, groovy 不是太熟,但是使用 groovy 肯定灵活性更高。
Spring Cloud Contract 默认会去 src/test/resources/contracts 目录下去加载契约文件,这里简单一点我们就不改目录了, 直接在这个目录下创建契约文件 getUser.yml(契约文件的具体内容,还需要根据你实际的接口规则去编写,此处返回的状态等都只适合我的测试代码,你可以组织各种各样不同的参数提交来模拟各种复杂情况,以提高测试的代码覆盖率)
## 此文件为 get user by userUuid 接口的契约
## 测试用户不存在
request:
method: GET
url: /user/33333333
headers:
Content-Type: application/json
response:
status: 500
body:
errorCode: 990004
headers:
Content-Type: application/json;charset=UTF-8
---
## 测试用户正常获取
request:
method: GET
url: /user/11111111
headers:
Content-Type: application/json
response:
status: 200
body:
errorCode: 0
errorMsg: SUCCESS
data:
userUuid: 11111111
userName: 张三
account: zhangsan
headers:
Content-Type: application/json;charset=UTF-8
契约编写完成之后,直接 mvn clean install 编译,如果成功,你可以在 代码目录的 target 目录下看到一个叫做 XXXX-stub.jar 的文件,这个 stub 文件就是你可以交给服务消费者使用的文件,你可以把它放到你们自己的 maven 仓库中,供别人下载。
然后,你可以在 target\generated-test-sources 找到一个 ContractVerifierTest 的类,它 extends 你写的 UserControllerTest 类,这里面,就是根据契约自动生成的测试用例。
服务消费方:
服务消费方关键就是要引入服务提供方给出的 stub 文件,有远程和本地两种引入方式。
首先需要引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
本地引入 stub 时,需要先获取 服务提供方的代码然后编译完成,即保证本地的 maven 仓库中有对应的 stub 文件。
然后编写消费方的测试代码:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureStubRunner(ids = {"com.walli:cloud-user-service-server:1.0.1:stubs:9900"},
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class SsoControllerTest {
private static final Logger LOGGER = LoggerFactory.getLogger(SsoControllerTest.class);
private ObjectMapper mapper;
@Autowired
public MockMvc mvc;
@Before
public void setUp() {
LOGGER.info("SsoControllerTest init");
this.mapper = new ObjectMapper();
}
@Test
public void testLogin() throws Exception {
this.mvc.perform(MockMvcRequestBuilders.get("/user/111111")
.contentType(MediaType.APPLICATION_JSON)
.content(this.mapper.writeValueAsString(param))
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("errorCode").value(0))
.andReturn();
}
}
其中
@AutoConfigureStubRunner(ids = {"com.walli:cloud-user-service-server:1.0.1:stubs:9900"},
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
这一段即为本地使用 stub 文件,如果是远程调用,需要按照如下方式进行:
首先在 application.yml 文件中声明stub依赖方式:
stubrunner:
ids: 'com.walli:cloud-user-service-server:1.0.1:stubs:9900'
repositoryRoot: http://repo.spring.io/libs-snapshot
repositoryRoot 改成自己的,然后把测试代码上的 @AutoConfigureStubRunner 中StubsMode 改成 REMOTE即可。
编译运行测试用例通过,即表示消费方契约测试成功,因为你并没有启动 cloud-user-service-server,但是你的测试用例还是通过了,并且调用的接口返回值是契约中约定的值。
消费方的契约测试,对于契约的要求比较严格,甚至于 headers 中的每一项都需要匹配上,当然这也有利于提高接口定义的规范程度。
来源:CSDN
作者:_Walli_
链接:https://blog.csdn.net/zxcvqwer19900720/article/details/103925749