分布式研发模型演进

  • 众所周知, 分布式系统是由众多微服务构成,并按照功能模块划分后, 由不同的开发小组进行维护. 研发模型如下图所示: 开发人员完成某一个微服务的功能后, 发布测试环境交付测试团队验证. 这种工作模式的弊端是, Bug在测试环境才被暴露, 而不是在编码阶段就被发现.

    Spring Cloud Contract 契约测试实践_spring
  • 为了解决上述的弊端, 研发团队通常会引入了单元测试, 并使用EasyMock, Mokito等框架, 来帮助开发人员在开发阶段暴露Bug. (对DB, Redis等依赖通常使用Docker来解决, 与主题无关, 这里暂时不做过多介绍. 有兴趣的可以自己研究)

    Spring Cloud Contract 契约测试实践_spring_02
  • 在日常的研发工作中, 很多团队或多或少遇到过这种情形: 微服务提供方修改了对外接口, 导致消费方无法正常请求, 造成生产事故. 管理上的人为避免, 难免导致各种疏漏, 为此我们找到了一种智能的解决方案---消费者驱动的契约测试. 大意是这样的: 服务提供方和消费方约定共同的契约, 双方围绕契约, 进行各自的单元测试工作.

    Spring Cloud Contract 契约测试实践_spring_03

Spring Cloud Contract概要

  • 永辉云创使用Spring Cloud作为微服务基础框架, 借助Spring Cloud Contract来帮助服务提供方和消费方来制定契约. 所谓契约, 就是双方约定好的接口调用参数, 及对应的输出. 整体概览如下图所示.

    Spring Cloud Contract 契约测试实践_spring_04
  • 通过上图, 相信大家对Spring Cloud Contract有了大体的了解, 下面我们用几个关键词来描述Spring Cloud Contract的特性.

    • 用于UT

    • 定义远程服务数据

    • 自动生成测试代码

  • Spring Cloud Contract在永辉云创的具体实施步骤如下图所示, 通常, 服务提供方, 也是数据定义方. 在这里, 我们使用的了数据定义方(所有服务契约在一个工程中定义), 服务提供方, 服务消费方三方模型. 

    Spring Cloud Contract 契约测试实践_spring_05

Spring Cloud Contract实践

以下内容,摘自我们推进Spring Cloud Contract落地之初,编写的技术文档。 希望给读者带来更加接地气的参考, 部分内容进行了脱敏, 请读者谅解.

数据定义方

对于请求返回数据, 所有提供方统一在spring-cloud-contract(内部项目名, 非spring cloud Contract)项目里定义, 方便大家看测试数据

Spring Cloud Contract 契约测试实践_spring_06

原则上由服务开发定义者来提供这个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/

Spring Cloud Contract 契约测试实践_spring_07

服务提供方

引入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生成测试代码报错的问题)

Spring Cloud Contract 契约测试实践_spring_08

<!-- 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");
    }

}

 

 

 

 

Spring Cloud Contract 契约测试实践_spring_09