查看原文
其他

Spring Cloud Contract 契约测试实践

程序猿DD 2019-07-13

本文转载公众号:永辉云创技术

该号由我参与维护,欢迎大家关注支持!!!

分布式研发模型演进

  • 众所周知, 分布式系统是由众多微服务构成,并按照功能模块划分后, 由不同的开发小组进行维护. 研发模型如下图所示: 开发人员完成某一个微服务的功能后, 发布测试环境交付测试团队验证. 这种工作模式的弊端是, 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");
   }

}

热文推荐

全球最大同性交友网站 GitHub 10 岁了!

JDK 1.5 - 1.8 各版本的新特性总结

Spring Boot快速开发利器:Spring Boot CLI

IntelliJ IDEA 2018.1正式发布!还能这么玩?

消息中间件选型分析

自建API网关「架构设计篇」

其他推荐

Spring Cloud构建微服务架构:分布式配置中心(加密解密)

Spring Boot使用@Async实现异步调用:线程池的优雅关闭

Spring Boot使用@Async实现异步调用:自定义线程池

Spring Boot 2.0正式发布,升还是不升呢?

Spring Boot 2.0 新特性概览

Spring Boot/Cloud干货汇总

长按指纹

一键关注

深入交流、更多福利

扫码加入我的知识星球



点击 “阅读原文” 看看本号其他精彩内容

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存