Spring Cloud Contract 契约测试实践
本文转载公众号:永辉云创技术
该号由我参与维护,欢迎大家关注支持!!!
分布式研发模型演进
众所周知, 分布式系统是由众多微服务构成,并按照功能模块划分后, 由不同的开发小组进行维护. 研发模型如下图所示: 开发人员完成某一个微服务的功能后, 发布测试环境交付测试团队验证. 这种工作模式的弊端是, Bug在测试环境才被暴露, 而不是在编码阶段就被发现.
为了解决上述的弊端, 研发团队通常会引入了单元测试, 并使用EasyMock, Mokito等框架, 来帮助开发人员在开发阶段暴露Bug. (对DB, Redis等依赖通常使用Docker来解决, 与主题无关, 这里暂时不做过多介绍. 有兴趣的可以自己研究)
在日常的研发工作中, 很多团队或多或少遇到过这种情形: 微服务提供方修改了对外接口, 导致消费方无法正常请求, 造成生产事故. 管理上的人为避免, 难免导致各种疏漏, 为此我们找到了一种智能的解决方案---消费者驱动的契约测试. 大意是这样的: 服务提供方和消费方约定共同的契约, 双方围绕契约, 进行各自的单元测试工作.
Spring Cloud Contract概要
永辉云创使用Spring Cloud作为微服务基础框架, 借助Spring Cloud Contract来帮助服务提供方和消费方来制定契约. 所谓契约, 就是双方约定好的接口调用参数, 及对应的输出. 整体概览如下图所示.
通过上图, 相信大家对Spring Cloud Contract有了大体的了解, 下面我们用几个关键词来描述Spring Cloud Contract的特性.
用于UT
定义远程服务数据
自动生成测试代码
Spring Cloud Contract在永辉云创的具体实施步骤如下图所示, 通常, 服务提供方, 也是数据定义方. 在这里, 我们使用的了数据定义方(所有服务契约在一个工程中定义), 服务提供方, 服务消费方三方模型.
Spring Cloud Contract实践
以下内容,摘自我们推进Spring Cloud Contract落地之初,编写的技术文档。 希望给读者带来更加接地气的参考, 部分内容进行了脱敏, 请读者谅解.
数据定义方
对于请求返回数据, 所有提供方统一在spring-cloud-contract(内部项目名, 非spring cloud Contract)项目里定义, 方便大家看测试数据
原则上由服务开发定义者来提供这个groovy,但是如果时间急迫,依赖方直接编写,并有服务开发者review后也可以提交~
题外话:有些工具, 例如wiremock可以帮助录制并模拟http请求. 使用场景: 前端开发依赖于服务端提供的接口, 我们通常是等服务端开发完成后,部署到测试环境,供前端调用. 现在有了wiremock, 假设我们要开发v2版本的接口, 可以先录制v1版本的请求, 然后修改胶片为v2版本http响应. 这样就可以前端就可以在v2接口开发完成前, 愉快地进行mock请求, 减少前端对服务端接口进度的依赖.*
http://www.cnblogs.com/tanglang/p/4791198.html
http://wiremock.org/docs/running-standalone/
服务提供方
引入UT相关jar包
<!-- 集成wireMock来实现mock请求响应。wireMock会自动构建一个虚拟远程服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
<!-- 提供打包预定义数据服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<!-- 自动生成单元测试代码 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<!-- 依赖数据定义方 -->
<dependency>
<groupId>com.yonghui</groupId>
<artifactId>spring-cloud-contract</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
配置UT代码生成器插件
该插件可以帮助我们生成自动化代码, 执行命令"mvn clean install -Dmaven.test.skip=false"后, 即可看到target目录自动生成的UT代码. 注意, 插件要>1.1.4.RELEASE, (该版本修复了long类型的dsl生成测试代码报错的问题)
<!-- UT代码生成器插件 -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>1.1.4.RELEASE</version>
<extensions>true</extensions>
<configuration>
<!-- packageWithBaseClasses 设置基类包目录,使用baseClassMappings替代,不使用 -->
<!--<packageWithBaseClasses>contract</packageWithBaseClasses>-->
<!--baseClassMappings 设置生成测试的基类。用包名的正则来进行匹配 -->
<contractsWorkOffline>true</contractsWorkOffline>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*</contractPackageRegex>
<baseClassFQN>contract.ContractBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
<!--basePackageForTests 设置测试的生成的位置 -->
<basePackageForTests>verifier.tests</basePackageForTests>
<contractDependency>
<groupId>com.yonghui</groupId>
<artifactId>spring-cloud-contract</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>stubs</classifier>
</contractDependency>
<!--contractsPath 设置contracts路径-->
<contractsPath>contracts/xxx-mst-center</contractsPath>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.12</version>
</dependency>
<dependency>
<groupId>com.yonghui</groupId>
<artifactId>spring-cloud-contract</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>stubs</classifier>
</dependency>
</dependencies>
</plugin>
配置UT基础类
生成UT代码时, 有需求是需要初始化数据库, 配置内置的redis, mysql. 我们使用相关的开源框架, 搭建了自己的UT基础类, 进行ut前的场景准备.
package contract.resources;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
import com.yonghui.junit.InmomeryDbResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
/**
* Created by luyunfei on 09/10/2017.
*/
public class LocationDbResource extends InmomeryDbResource {
public LocationDbResource() {
// 初始化内置mysql, UT执行时, 会使用flyway进行初始化相关的表
super(40200, "xxx_mst_center");
}
protected void before() throws Throwable {
super.before();
// 初始化这个UT msql的相关数据
runResourceFile(dbName, "sql/contract/mst_location.sql");
}
}
package contract;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
import com.yonghui.junit.RedisResource;
import com.yonghui.xxx.mst.center.api.impl.TestBootstrap;
import contract.resources.LocationDbResource;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
/**
* Created by luyunfei on 27/09/2017.
*/
(SpringRunner.class)
(classes = {TestBootstrap.class})
public class xxx_mst_centerFLocationServiceBase {
private WebApplicationContext context;
// 增加这一行即可在UT中引入内置mysql, 并执行初始化
public static final ExternalResource dbresource = new LocationDbResource();
// 增加这一行即可在UT中引入内置Redis
public static final ExternalResource resource = new RedisResource(20300);
public void setUp() throws Throwable {
// RestAssuredMockMvc.standaloneSetup(new AccountController());
RestAssuredMockMvc.webAppContextSetup(context);
}
}
Test文件夹下的项目启动类Bootstrap
需要注释掉consul, feign, 保证ut对外部依赖的隔离. 经过实践, 发现测试时TestBootstrap不会覆盖Bootstarp, 因此需要保持两者名字一致, 即TestBootstrap要修改文件名为Bootstrap.class
package com.yonghui.xxx.mst.center.api.impl;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.context.annotation.ComponentScan;
/**
* Created by luyunfei on 11/04/2017.
*/
// 注意不要用SpringCloudApplication, 它会依赖consul启动, 而ut中不需要启动consul
//@SpringCloudApplication
// 下面这个要注释掉, 其它和Bootstrap一样
// @Import({YhConsulConfig.class,FeignConfiguration.class})
// 需要引入FeignConfiguration.class, 同时增加配置spring.application.feature.enabled=false
({FeignConfiguration.class})
(basePackages = "com.yonghui.xxx")
("com.yonghui.xxx.mst.center.mapper")
public class Bootstrap {
private static final Logger log = LoggerFactory.getLogger(TestBootstrap.class);
public static void main(String[] args) {
SpringApplication.run(TestBootstrap.class, args);
log.info("Bootstrap started successfully");
}
}
test/resources/bootstrap.properties__增加配置(重要) __
spring.cloud.consul.enabled=false
spring.application.feature.enabled=false
服务消费方
配置和服务提供方一致, 需要调用提供方接口的测试类, 增加以下注释, 端口号不要写错了
"com.yonghui:xxx-mst-center-server:1.0-SNAPSHOT:stubs:5656"} ,workOffline = true)
(ids = {package contract;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
import com.yonghui.junit.InmomeryDbResource;
import com.yonghui.junit.RedisResource;
import com.yonghui.xxx.inventory.center.api.impl.TestBootstrap;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
/**
* Created by luyunfei on 28/09/2017.
*/
(SpringRunner.class)
(classes = {TestBootstrap.class})
(ids = {"com.yonghui:xxx-mst-center-server:1.0-SNAPSHOT:stubs:5656"}
,workOffline = true)
public class Xxx_inventory_centerFInventoryServiceBase extends InmomeryDbResource {
private WebApplicationContext context;
public static final ExternalResource resource = new RedisResource(20300);
public xxx_inventory_centerFInventoryServiceBase() {
super(40200, "xxx_inventory_center");
}
public void setup() throws Throwable {
// RestAssuredMockMvc.standaloneSetup(new AccountController());
RestAssuredMockMvc.webAppContextSetup(context);
super.before();
// 初始化sql
//runResourceFile(dbName, "sql/DockServiceImplTest01/dockServiceGetListTest01.sql");
}
}
热文推荐:
Spring Boot快速开发利器:Spring Boot CLI
IntelliJ IDEA 2018.1正式发布!还能这么玩?
其他推荐:
Spring Cloud构建微服务架构:分布式配置中心(加密解密)
Spring Boot使用@Async实现异步调用:线程池的优雅关闭
Spring Boot使用@Async实现异步调用:自定义线程池
长按指纹
一键关注
深入交流、更多福利
扫码加入我的知识星球
点击 “阅读原文” 看看本号其他精彩内容