分布式研发模型演进
-
众所周知, 分布式系统是由众多微服务构成,并按照功能模块划分后, 由不同的开发小组进行维护. 研发模型如下图所示: 开发人员完成某一个微服务的功能后, 发布测试环境交付测试团队验证. 这种工作模式的弊端是, 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");
}
@Override
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.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {TestBootstrap.class})
public class xxx_mst_centerFLocationServiceBase {
@Autowired
private WebApplicationContext context;
// 增加这一行即可在UT中引入内置mysql, 并执行初始化
@ClassRule
public static final ExternalResource dbresource = new LocationDbResource();
// 增加这一行即可在UT中引入内置Redis
@ClassRule
public static final ExternalResource resource = new RedisResource(20300);
@Before
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.
*/
@EnableAutoConfiguration
// 注意不要用SpringCloudApplication, 它会依赖consul启动, 而ut中不需要启动consul
//@SpringCloudApplication
@SpringBootApplication
// 下面这个要注释掉, 其它和Bootstrap一样
// @Import({YhConsulConfig.class,FeignConfiguration.class})
// 需要引入FeignConfiguration.class, 同时增加配置spring.application.feature.enabled=false
@Import({FeignConfiguration.class})
@ComponentScan(basePackages = "com.yonghui.xxx")
@MapperScan("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
服务消费方
配置和服务提供方一致, 需要调用提供方接口的测试类, 增加以下注释, 端口号不要写错了
@AutoConfigureStubRunner(ids = {"com.yonghui:xxx-mst-center-server:1.0-SNAPSHOT:stubs:5656"} ,workOffline = true)
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.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {TestBootstrap.class})
@AutoConfigureStubRunner(ids = {"com.yonghui:xxx-mst-center-server:1.0-SNAPSHOT:stubs:5656"}
,workOffline = true)
public class Xxx_inventory_centerFInventoryServiceBase extends InmomeryDbResource {
@Autowired
private WebApplicationContext context;
@ClassRule
public static final ExternalResource resource = new RedisResource(20300);
public xxx_inventory_centerFInventoryServiceBase() {
super(40200, "xxx_inventory_center");
}
@Before
public void setup() throws Throwable {
// RestAssuredMockMvc.standaloneSetup(new AccountController());
RestAssuredMockMvc.webAppContextSetup(context);
super.before();
// 初始化sql
//runResourceFile(dbName, "sql/DockServiceImplTest01/dockServiceGetListTest01.sql");
}
}