Spring Cloud 接口契约测试__Walli_的博客-程序员秘密_springcloud怎么测试接口

技术标签: 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

智能推荐

软件项目开发流程以及人员职责_zhangbijun1230的博客-程序员秘密

软件项目开发流程以及人员职责2008年03月07日 12:05:00阅读数:29879实行软件工程项目管理:▲ 项目经理(负责人):项目经理(负责人)对整个项目负完全责任,是指导、控制、管理和规范某个软件和软/硬件系统建设的人,项目经理(负责人)是最终对客户负责的人。▲ 软件项目经理(负责人):软件项目经理(负责人)对一个项目的所有软件活动负完全责任,控制一个项目的所有软件资源,按照软件约定与项目...

Leetcode day4:打家劫舍III_hit1180300517的博客-程序员秘密

原题:在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。(话说这个小偷这么聪明为什么不去当程序员)示例 1:解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.示例 2:

频繁项集挖掘算法在告警关联中的应用_云智慧AIOps社区的博客-程序员秘密_频繁项挖掘

# 技术黑板报 # 第十期推荐阅读时长:15min在上一篇技术黑板报中,我们介绍了频繁项集挖掘这一问题,并讲解了Apriori算法与FP-Growth算法的技术原理。本期技术黑板报我们将主要围绕频繁项集挖掘算法的实际应用,即当该算法应用到告警关联场景中时,我们遇到了哪些问题,如何解决这些问题,以及我们如何在原始FP-Growth算法的基础上进行改进,从而研发了专用于告警关联场景下的CW-FP-Growth算法。为了展示该算法的实际效果,我们在文末给出了这一算法在脱敏数据中的案例。..

AlertManager实现webhook告警(使用Postman测试)_程序员小王java的博客-程序员秘密_alertmanage webhook

AlertManager实现webhook告警(使用Postman测试),Alertmanager 主要用于接收 Prometheus 发送的告警信息,它支持丰富的告警通知渠道,而且很容易做到告警信息进行去重,降噪,分组等,是一款前卫的告警通知系统。但是我们公司内部不使用Prometheus,只使用AlertManager.

Java基础__Java中异常处理那些事_weixin_30414635的博客-程序员秘密

一、Exception 类的层次所有的异常类是从 java.lang.Exception 类继承的子类。Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。Error 用来指示运行时环境发生的错误。异常类有两个主要的子类:IOException 类和 RuntimeException 类。...

随便推点

架构设计 - 自动化运维之架构设计六要点_数据和云的博客-程序员秘密

运维自动化是我们所渴望获得的,但是我们在一味强调自动化能力时,却忽略了影响自动化落地的一个关键因素。那便是跟运维朝夕相处,让人又爱又恨的业务架构。因为业务架构是决定运维效率和质量的关键因素之一,所以我想跟大家一起聊一下怎么样的架构设计是对运维友好的。结合这些年在腾讯遇到的业务架构和做运维规划时对业务非功能规范的思考,我们可以把面向运维的架构设计分成六大设计要点。

解决word或wps办公软件删除空白页后页面布局变乱问题_归零-li的博客-程序员秘密

在word或wps文档使用中,我们通常用使用Delete或Backspace键删除空白页,但是有时候会遇到删除后其他页面布局变乱的情况,为了能使其他页面正常显示,需要通过以下方式解决:对于办公软件的使用,确实总能遇到很多不大懂处理的问题,绝大部分通过百度还是可以解决的,但需要花费一定的时间去查找,把问题及解决的方法记下来,以后可以少走些弯路~...

pandas 常用的数学统计方法 mad()_诗雨时的博客-程序员秘密_mad法

pandas 常用的数学统计方法 mad()1、定义:根据平均值计算平均绝对距离差。2、示例: import pandas as pdstudent_info = pd.read_csv("F:/人工智能/科学计算库/files/student_info.csv")pri...

什么是DevOps?_老杨的码农生活的博客-程序员秘密_devops什么意思

最近的DevOps的概念很火,大家都在讨论DevOps,有人说DevOps是自动化运维,有人说DevOps是流程和管理,还有人说DevOps是一种文化,以前的运维工程师也纷纷变成了DevOps工程师。 那DevOps究竟是什么?传统的运维模式以及面临的挑战 在传统的瀑布模型开发中,软件生命周期中的运行维护工作是由运维工程师来完成的。 开发人员完成编码,测试...

从Halcon到Opencv(python)_fill_up_shape_Kevin_liuj的博客-程序员秘密

主要记录一下从Halcon的一些算例在opencv中的实现Halcon中的例子:fill_up_shape实现的目的如下:原图:最终要输出的我改了一下:代码实现如下:首先定义一个通过Trackbar以面积为特征的选择函数,从而输出选定区域.def show_selectShape(image,binary,method = 'area',scale = 1.0): if method == 'area': num_labels, labels, stats, ce

html报警函数,alertmanager自定义告警模板(五)_心安王的博客-程序员秘密

一、告警模板alertmanager是可以自定义告警模板的。注意钉钉需要部署prometheus-webhook-dingtalk,这个也是支持模板的,但是模板要写在prometheus-webhook-dingtalk里,而不是alertmanager项目链接https://github.com/timonwong/prometheus-webhook-dingtalk通过配置templates...

推荐文章

热门文章

相关标签