Spring Cloud 接口契约测试_spring cloud 接口测试-程序员宅基地

技术标签: DBUnit  Spring Cloud Contract  Spring Cloud  

在微服务体系中,开发者要进行接口测试,一般有以下几种方法:

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

文档:https://cloud.spring.io/spring-cloud-static/Greenwich.RELEASE/single/spring-cloud.html#_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,但是你的测试用例还是通过了,并且调用的接口返回值是契约中约定的值。

 

关键概念&知识点

1. priority 数值越小,优先级越高

    在契约定义中,我们有时候不仅要定义接口的正常返回值,也有可能要定义异常的返回值(即消费者按照此规则传递参数之后获得的是一个错误),这种情况我们可以利用 priority,将错误返回值的契约设置较高优先级,将正常返回值的契约优先级降低,这样有利于消费者需求错误响应时能拿到相应的错误,而不是匹配上正确的响应结果。

2. request 中的一些概念

    request 中的参数,不管是 queryParameters 还是 body 中的参数,如果你不写相应的 matcher 的话,契约中就是默认生成参数必须严格相等的契约。

    request.matchers 是用来定义请求参数的规则的,即消费者必须按照此规则来提交参数;并且,matchers 中最好只定义必传参数的规则,否则就会面临不必传的参数,消费者使用契约时必须填写该参数才行;契约中不存在的参数,默认都是可传可不传的。

    request matchers queryParameters 的 type 可用如下值:
    equal_to_json, equal_to, not_matching, matching, containing, absent, equal_to_xml

3. response 中的一些概念

    response.body 的返回值,是用来模拟给消费者的返回值的,同时也用来校验spring cloud contract 自动生成测试用例的返回结果是不是跟这个值匹配

    response.matchers 的用处是当 spring cloud contract 自动生成测试用例得到的返回结果与 response.body 中定义的值不一样时,比如创建用户生成 uuid,这个 uuid 必然是随机的,response.body 必然无法自定义,所以这时候需要写 response.matchers 定义 uuid 规则,只要实际的返回值符合这个规则,测试用例就认为可以通过。matchers 不特殊定义匹配规则的字段,就是严格等于 response.body 中定义的值

    response.body 中最好写调用此接口必然会返回的值,同时 response.matchers 中需要对应 response.body 返回的字段有动态值的字段写好相应的 matcher,否则自动生成的测试用例无法通过

    response matchers  type 可用如下值:
    by_type, by_command, by_time, by_date, by_timestamp, by_null, by_equality, by_regex

    response matchers type=by_regex时,使用 predefined 可用如下预定义的正则表达式:
    non_blank, iso_date_time, iso_8601_with_offset, iso_time, iso_date, only_alpha_unicode, url, hostname, any_boolean, uuid, ip_address, any_double, number, non_empty, email

 

4. matchers 中,url 无法用正常的正则表达式(关键是不能用 ^ 和 $,包括 groovy 中也是如此),YML可以

 

局限性

1. 在类似 新增/更新 这种参数不确定的请求中,request 的参数只能写必传值,response 也只能写必然会返回的数据(更新可以所有有效字段)。

2. reuqest.queryParameters 无法编写数组类型的参数契约,只能默认全都允许(即 request matchers 中判断其不为空即可)

3. request.body 中的参数对象如果每个字段都可为空,这种契约是没法编写的,契约的 request.body 不写的话代码会报 body 不能为空的错误,写的话又强制了定下契约body的字段必须传递,这是个矛盾点,所以只能挑一个绝大多数情况下都会传递的参数写到 request.body 里面,消费者调用的时候传递一下这个参数。

 

实践过程中遇到的问题

1. 单元测试在 jenkins 脚本中编译可以,但是测试用例完全无法运行,出错的现象是无法访问其他微服务,比如 config-server。

解决办法:

这个问题困扰了很久,最后发现是编译时使用的maven镜像并没有加入整个Spring Cloud 的网络环境中。整体的环境是,Docker中启动Jenkins,Jenkins 创建多流水线任务调用项目代码中的 Jenkinsfile,Jenkinsfile 中定义 Pipeline,最开始,pipeline 中使用的 agent 如下:

pipeline {
    agent {
        docker {
            image 'maven:3.6.0-jdk-8'
            args '-v /root/.m2:/root/.m2'
        }
    }
    。。。。。。
}

可以看出来使用的官方的 maven 镜像来进行编译工作,但是这个镜像是官方的,并没有加入我们自定义的SpringCloud使用的网络中,于是在 args 中增加网络参数,即可解决

pipeline {
    agent {
        docker {
            image 'maven:3.6.0-jdk-8'
            args '-v /root/.m2:/root/.m2 --net=servicenet'
        }
    }
    。。。。。。
}

2. DBUnit 解析的时间不对,xml 数据中,以“yyyy-MM-dd HH:MM:SS” 的格式写入时间,代码中会自动将其转换成UTC时间格式,发现在本地开发的时候,正常转换没有问题,UTC字符比实际的时间少8小时,但是在Jenkins中编译时,UTC字符跟xml里面时间是一样的,导致了测试用例失败。

解决办法:

其实就是个时区的问题,jenkins 通过 Docker 容器启动,Docker 容器没有设置为正确的时区的话,openjdk8默认是从系统的 /etc/timezone 文件中读取时区的,因此造成了 xml 中的时间没有被正确的解析,默认的 timezone 就是0时区。解决办法是启动jenkins时,把相应的时间、时区都设置进去,比如 docker-compose.yml:

version: '3'

services:
 jenkins:
    image: wx.ankoninc.com.cn/jenkins:2.222.4
    user: root
    privileged: true
    container_name: jenkins
    environment:
        # 这一行也很重要,告诉 JVM 时区
        JAVA_OPTS: -Duser.timezone=Asia/Shanghai
    volumes:
      # 容器同步宿主机时间
      - /etc/localtime:/etc/localtime
      # 容器同步宿主机时区
      - /etc/timezone:/etc/timezone
      - jenkins-data:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      - /root/.docker:/root/.docker
    ports:
      - 8080:8080
      - 50000:50000
    networks:
      - servicenet


volumes:
  jenkins-data:
    driver: local

networks:
  servicenet:
    external: true

如果宿主机中 timezone 文件不存在,自己创建一个即可

echo "Asia/Shanghai" >> /etc/timezone

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zxcvqwer19900720/article/details/103925749

智能推荐

实验记录 | scATAC-seq数据的比对(二)_scatac-seq分析流程cellranger-atac比对数据三个r3怎么办-程序员宅基地

文章浏览阅读510次。1。重新核实咱们感兴趣的序列属于哪一个家族?首先需要建库。bowtie2-build humrep.fasta HumRepbowtie2 -f --local --very-sensitive -x HumRep -U aluY.fasta -S aluY.samsamtools view -b aluY.sam >aluY.bam#这次就没有报错了。但是仍然存在一些问题。(base) [xxzhang@mu02 RepeatAnnoation]$ grep "ALU" aluY.sa_scatac-seq分析流程cellranger-atac比对数据三个r3怎么办

springmvc框架知识重点_springmvc框架的重点-程序员宅基地

文章浏览阅读1k次,点赞2次,收藏6次。1. 什么是springmvc?Springmvc是一个基于mvc的web框架,它是spring框架的一个模块,两者无需通过中间整合进行整合。2. Springmvc的优点(1)Spring MVC中提供一个DispatcherServlet, 无需额外开发。(2)springMVC中使用基于xml的配置文件,可以编辑,无需重新编译应用程序。(3)springMVC实例化控制器,并根据用..._springmvc框架的重点

与设备联调Csharp使用SerialPort这个类实现的串口通讯时出现的数据不完整问题解决。_上位机发送的数据接收不完整-程序员宅基地

文章浏览阅读1.7k次。现象:环境:上位机软件,Csharp编写,SerialPort类实现串口异步通讯;下位机,STM32F1系列单片机。串口设置: 波特率9600, 8bit数据位,1bit停止位, 无校验。当上位机与下位机串口通讯时,通过观察收发,发现有时候接收的数据并不是一个完整的数据帧。例如: 发 10 11 12 13 14收到可能为 10 11 12然后再收到 13 14.但是通过串口助手..._上位机发送的数据接收不完整

MySQL的auto_increment使用_auto increment怎么用-程序员宅基地

文章浏览阅读4.1k次,点赞2次,收藏14次。说明总结自《mysql技术内幕(第5版)》创建auto_increment列要遵循如下规则每个表只能有一个列具有auto_increment属性,且必须为整数数据类型(当然,也支持浮点类型,但强烈不建议)该列必须建立索引,最常见的就是使用primary key或unique索引,当然,也支持不唯一索引该列必须是not null的auto_increment列将有如下特性用ins..._auto increment怎么用

String/StringBuilder/StringBuffer_valuestringbuilder-程序员宅基地

文章浏览阅读93次。String 、StringBuilder、StringBuffer1. 可变性String: 字符串常量,字符串是不可变的。源码中:private final char value[];//通过final修饰的字符数组来存储字符//java 9 改用private final byte[] valueStringBuilder: 字符串常量,是可变的,源码中://继承AbstractStringBuilderchar[] value;//AbstractStringBuilder中_valuestringbuilder

Excel转PDF工具类_pdfsaveoptions.setdefaultfont(-程序员宅基地

文章浏览阅读425次。public class ExcelToPDF { /** * 获取license * * @return */ private static boolean getLicense() { boolean result = false; try { InputStream license = ExcelToPDF.class.getClassLoader().getResourceAsStrea_pdfsaveoptions.setdefaultfont(

随便推点

vue数据的双向绑定和组件化开发_vue2 组件开发数据双休绑定-程序员宅基地

文章浏览阅读337次。今日内容表单输入绑定数据双向绑定​ v-model 只能应用在input textare select 表单控件中<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" con..._vue2 组件开发数据双休绑定

@JsonProperty 注解的用法_jsonproperty注解参数说明-程序员宅基地

文章浏览阅读2.1k次,点赞2次,收藏4次。做后端的各位对于JSON格式传参肯定不陌生吧?咱先看一组大家常用的入参:【ps:csdn博客的代码块不支持JSON?待改进嗷】{ "loginNo": "admin", "debugEndDate" : "2050-7-20 18:25:22", "debugStartDate" : "2010-7-1 09:25:22", "type" : "1"}那么对于这么一组入参,后端获取的方法特别简单:1、构建对应的实体类,个人这边建议使用IDEA配套_jsonproperty注解参数说明

两个html之间通过url传值_html两个页面之间通过url传值-程序员宅基地

文章浏览阅读2.5k次。第一个html中写跳转htmlwindow.location.href='localhost:8080/xx/xx.html?orgid=1&amp;orglevel=2&amp;orgseq=3' 目标html取值://解析url路径,获取参数 function getURLParameter(name) { return decodeURIComponent((ne..._html两个页面之间通过url传值

实验记录 | 8/5_variant quality-程序员宅基地

文章浏览阅读240次。忙活好现在一堆乱七八糟的事情,到现在已经将近11点了。那么今天就从现在开始。首先回顾前两天做的事情。前两天主要就是:(1)使用CML的其他数据,再次验证在已知标签的情况下,我们能不能将这些样本聚类在一起,也就是说验证sclinager的可行性。==>现在这部分的数据已经在运行,预计8.7结束可以完成这28个样本的运行,并得到阶段性的结果。按照已知类别标签标记的热图,看是否可以聚在一起。(2)另一方面,想要尝试优化这个流程。最重要的是时间方面的,有无必要砍掉预处理的环节,因为时间消耗最大._variant quality

获取IOC容器对象总结_ioc.get-程序员宅基地

文章浏览阅读1.2k次,点赞4次,收藏2次。在项目中如果不能直接autowired依赖注入的时候(例如利用了其他框架的监听器),需要自己注入IOC容器,来获取容器管理的bean对象,以下总结了两个个方法~方法1ContextLoaderListener 监听器在服务器启动的时候会监听ServletContext对象的创建,去创建IOC容器对象,并且将IOC容器对象放在Application域中所以可以通过以下方式获取 Applica..._ioc.get

三种方式打开命令行窗口_打开代码窗口的方法-程序员宅基地

文章浏览阅读1w次,点赞3次,收藏5次。1.按住Shift键右击鼠标打开命令行窗口2.任意文件夹打开拖拽。cd +路径3.当前文件夹输入cmd 进入命令窗口_打开代码窗口的方法