调用方和服务方约定好接口,生成映射文件,这个文件即可以用于客户端模拟服务,也可以用于服务方集成测试,这样双方开发也好、集成也好都会方便很多。下面我们来研究一下 Spring Cloud Contract,它就是基于 WireMock 实现了契约式的测试,上文中双方约定好的接口,其实就是双方的契约。

微服务的集成

前面已经提到,传统方式下,微服务的集成以及测试都是一件很头痛的事情。其实在微服务概念还没有出现之前,在 SOA 流行的时候,就有人提出了消费者驱动契约(Consumer Driven Contract,CDC)的概念。微服务流行后,服务的集成和集成测试成了不得不解决问题,于是出现了基于消费者驱动契约的测试工具,最流行的应该就是 Pact,还有就是今天我们要说的 Spring Cloud Contract。

消费者驱动契约

熟悉敏捷开发的同学应该知道,敏捷开发提倡测试先行,相应的提出了不少方法和流程,例如测试驱动开发(Test Driven Design,TDD)、验收测试驱动开发(Acceptance Test Driven Development,ATDD)、行为驱动设计(Behavior Driven Design,BDD )、实例化需求(Specification By Example)等等。它们的共同特点在开发前就约定好了各种形式的契约。如果是单元测试作为契约,就是 TDD;如果是验收测试作为契约,就是 ATDD;如果是形式化语言甚至图表定义的业务规则,那就是 BDD 或者实例化需求。
对于基于 HTTP 的微服务来说,它的契约就是指 API 的请求和响应的规则。对于请求,包括请求 URL 及参数,请求头,请求内容等;对于响应,包括状态码,响应头,响应内容等。
在 Spring Cloud Contract 里,契约是用一种基于 Groovy 的 DSL 定义的。例如下面是一个短信接口的契约(省略了部分内容,例如 Content-Type 头等)。

org.springframework.cloud.contract.spec.Contract.make {
    // 如果消费方发送了一个请求
    request {                 
        // 请求方法是 POST               
        method 'POST'    
        // 请求 URL 是 `/sendsms`                   
        url '/sendsms'       
        // 请求内容是 Json 文本,包括电话号码和要发送的文本                
        body([         
               // 电话号码必须是13个数字组成                      
               phone: $(regex('[0-9]{13}')),  
               // 发送文本必须为"您好"
               content: "您好"                 
        ])
    }
    response {
        // 那么服务方应该返回状态码 200
        status 200        
        // 响应内容是 Json 文本,内容为 { "success": true }                   
        body([                               
               success: true
        ])
    }}

使用 CDC 开发服务的大致过程是这样的。

  1. 编写契约(Groovy 的 DSL 定义(提供方)---业务方和服务方相关人员一起讨论。业务方告知服务方接口使用的场景、期望的返回是什么,服务方考虑接口方案和实现,双方一起定下一个或多个契约。

  2. 契约提供者自验证(提供方)---确定了契约之后,Spring Cloud Contract 会给服务方自动生成验收测试,用于验证接口是否符合契约。服务方要确保开发完成后,这些验收测试都能够通过。

  3. 消费方通过stub进行集成测试(消费方)---服务消费方也可以基于这个契约开始开发功能。Spring Cloud Contract 会基于契约生成 Stub 服务,这样业务方就不必等接口开发完成,可以通过 Stub 服务进行集成测试。

 契约测试SpringCloud Contract入门_json

所以 CDC 和行为驱动设计(BDD)很类似,都是从使用者的需求出发,双方订立契约,测试先行的开发方法。不过一个是针对系统的验收,一个是针对服务的集成。CDC 的好处有以下几点:

  • 让服务方和调用方有充分的沟通,确保服务方提供接口都是以调用方的需求出发,并且服务方的开发者也可以充分理解调用方的使用场景。

  • 解耦和服务方和调用方的开发过程,一旦契约订立,双方都可以并行开发,通过 Mock 和自动化集成测试确保双方都遵守契约,最终集成也会更简单。

  • 通过 Mock 和自动化测试,可以确保双方在演进过程中,也不会破坏已有的契约。


但是要注意一点是,契约不包括业务逻辑,业务逻辑还是需要服务方和调用方通过单元测试、其他集成测试来确保。例如上面的短信服务,可能服务方会有一个逻辑是每天一个号码最多发送一条短信,但这个逻辑并不会包含在契约里,可能契约只有包含成功和错误两种情况。

Spring Cloud Contract 使用方法

我使用的

springboot是1.5.10.RELEASE

springcloud是Edgware.SR3

服务提供方:

第一步:环境准备:JarSpring Cloud Contract 支持 Gradle 和 Maven,下面是gradle

1、buildscript的dependencies中增加:

testCompile 'org.springframework.cloud:spring-cloud-contract-gradle-plugin:1.2.7.RELEASE'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'

2、增加plugin:

apply plugin: 'application'
apply plugin: 'groovy'
apply plugin: 'spring-cloud-contract'
apply plugin: 'maven'
apply plugin: 'maven-publish'

3、publishing:

publishing {
    repositories {
        repositories {
            maven { url 'https://nexus.xxx.net/repository/contract-test/'
                credentials {
                   username 'user'
                    password 'xxxx'
                }
            }
        }
    }

    publications {
        maven(MavenPublication) {
            //指定group/artifact/version信息,可以不填。默认使用项目group/name/version作为groupId/artifactId/version
            groupId "$project.group"
            artifactId "$project.name"
            version "1.0.0-SNAPSHOT"
            artifact verifierStubsJar
        }
    }
}

第二步:编写契约,下面2个示例

groovy定义的示例:在/src/test/resources/contracts目录下,创建test.groovy文件:

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        method GET()
        urlPath('/api/v1/getUserInfo') {
            queryParameters {
                parameter('memberId', 'abc123')
            }
        }
        headers {
            contentType('application/json')
            header('myHeader', 'duan')
        }
    }
    response {
        status 200
        body([
            "respCode": "000000",
            "respMsg": "success"
          ])
        headers {
            //contentType('application/json')
        }
    }
}

示例sms:

org.springframework.cloud.contract.spec.Contract.make {
    // 如果消费方发送了一个请求
    request {                 
        // 请求方法是 POST               
        method 'POST'    
        // 请求 URL 是 `/sendsms`                   
        url '/sendsms'       
        // 请求内容是 Json 文本,包括电话号码和要发送的文本                
        body([         
               // 电话号码必须是13个数字组成                      
               phone: $(regex('[0-9]{13}')),  
               // 发送文本必须为"您好"
               content: "您好"                 
        ])
    }
    response {
        // 那么服务方应该返回状态码 200
        status 200        
        // 响应内容是 Json 文本,内容为 { "success": true }                   
        body([                               
               success: true
        ])
    }}

第三步:服务提供者自验证

对于服务提供方,Spring Cloud Contract 提供了一个叫 Contarct Verifier 的东西,用于解析契约文件生成测试代码,可以通过运行该测试代码进行契约的自验证。

testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
如果使用 Gradle 的话,通过以下命令生成测试。

./gradlew generateContractTests
或者gradle test
在build下生产junit的测试类:

上面发送短信的契约,生成的测试代码是这样的。如果有多个groovy文件,在ContractVerifierTest就会有多个方法

public class SmsTest extends ConstractTestBase {
    @Test
    public void validate_sendsms() throws Exception {
        // given:
            MockMvcRequestSpecification request = given()
                    .body("{\"phone\":\"2066260255168\",\"content\":\"\u60A8\u597D\"}");
        // when:
            ResponseOptions response = given().spec(request)
                    .post("/sendsms");
        // then:
            assertThat(response.statusCode()).isEqualTo(200);
        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).field("['success']").isEqualTo(true);
    }
}

可以看到是一个很标准的 JUnit 测试,使用了 RestAssured 来测试 API 接口。其中的 ConstractTestBase是设置的测试基类,里面可以做一些配置以及 Setup 和 Teardown 操作。例如这里,我们需要用 RestAssured 来启动 Spring 的 webApplicationContext,当然我也可以用 standaloneSetup 设置启动单个 Controller。

------------ConstractTestBase说明--------------------------------------

1、基类 上面自动生成的类的基类是需要自己编写

package com.xxx.xxx.contract;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;

import com.xxx.xxx.Application;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
//@ActiveProfiles("unit-test")  //当契约测试时想使用不同的配置文件时,取消注释时会使用loan-unit-test.yml配置文件
public abstract class ConstractTestBase {
    static{
        System.setProperty("aes.xxx", "abc");
    }

    @Autowired
    private WebApplicationContext context;
    @Before
    public void setUp() throws Exception {
        RestAssuredMockMvc.webAppContextSetup(context);
    } 
}

2、该类需要配置在gradle中:

contracts {
    baseClassForTests = 'com.xxx.xxx.contract.ConstractTestBase'
}

如果没有基类及baseClassForTests 的配置,在执行契约自验证时,会出现错误:

java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically

大概就是无法初始化MockMVC了。

------------ConstractTestBase说明--------------------------------------

执行契约的自验证测试的方法是:gradle test

查看报告方法:上面gradle test运行结束后,有个结果报告位置,打开这个html就可以看到了

 契约测试SpringCloud Contract入门_maven_02

 契约测试SpringCloud Contract入门_spring_03

第四步:上传契约的stub

首先我们需要在服务方通过以下命令生成 Stub 服务的 Jar 包。

d:\gitspace\loan (master -> origin)
λ gradle verifierStubsJar
Starting a Gradle Daemon (subsequent builds will be faster)
:copyContracts
:generateClientStubs
:verifierStubsJar

BUILD SUCCESSFUL

Total time: 18.885 secs
d:\gitspace\loan (master -> origin)
如果上面契约自验证通过了,在build/libs下生产stubs的jar。
契约测试SpringCloud Contract入门_json_04

 

 现在开始上传契约的stub到maven私服,提供给消费方使用。在项目根目录下执行gradle publish即可。

服务调用方
在上面生产的stubs的 Jar 包里面包含了契约文件以及生成的 WireMock 映射文件。我们可以把它发布到 Maven 私库里去,这样调用方可以直接从私库下载 Stub 的 Jar 包。

对于调用方,Spring Cloud Contract 提供了 Stub Runner 来简化 Stub 的使用。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
        webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@ActiveProfiles("unit-test")
@AutoConfigureStubRunner(
        repositoryRoot="https://xxx.xxx.net/repository/contract-test",
                ids = "com.xxx:loan:1.0.0-SNAPSHOT:stubs:8090")
public class ContractTest {
    @Test
    public void testSendSms() {
        ResponseEntity<SmsServiceResponse> response =
        restTemplate.exchange("http://localhost:6565/sendsms", HttpMethod.POST,
                new HttpEntity<>(request), SmsServiceResponse.class);
        // do some verification
    }
}

注意注解 AutoConfigureStubRunner,里面设置了下载 Stub Jar 包的私库地址以及包的完整 ID,注意最后的 6565 就是指定 Stub 运行的本地端口。测试的时候访问 Stub 端口,就会根据契约返回内容。

契约测试SpringCloud Contract入门_jar_05

还有一种方式:在/src/test/resources目录下增加一个配置文件application-stub.yml文件:

stubrunner:
  stubs-mode: REMOTE
  repositoryRoot: https://xxx.xxx.net/repository/contract-test
  username: user
  password: xxx
  ids:
    - com.xxx.xxx:1.0.0-SNAPSHOT:stubs:8090

 

在该配置下的junit写法如下:

import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import com.xxx.xxx.xxx.web.test.TestController;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner
@ActiveProfiles("stub")  //读取/src/test/resources/application-stub.yml的配置,远程使用默认的default
//@Ignore
public class ConsumerLoanContractTest {

    @Autowired
    private TestController testController;

    @Test
    public void pay() {
        String result = testController.testLoan("abc123");
        System.out.println("result=" + result);
        assert "0".equals(result);

    }

}

 


前端开发


另外一个使用 Mock 的场景就是对于前端开发。以前,前端工程师一般需要自己创建 Mock 数据进行开发,但 Mock 数据很容易和后台最终提供的数据有不一致的地方。CDC 和 Spring Cloud Contract 也可以帮上忙。
Spring Cloud Contract 生成的 Stub 其实是 WireMock 的映射文件,因此直接使用 WireMock 也是可以的。不过,它还提供了使用 Spring Cloud Cli 运行 Stub 的方式。
首先需要安装 SpringBoot Cli 和 Spring Cloud Cli,Mac 下可以使用 Homebrew。

$ brew tap pivotal/tap
$ brew install springboot
$ spring install org.springframework.cloud:spring-cloud-cli:1.4.0.RELEASE


然后在当前目录创建一个 stubrunner.yml 配置文件,里面的配置参数和前面的 AutoConfigureStubRunner 的配置其实是一样的:

stubrunner:
 workOffline: false
 repositoryRoot: http://<nexus_root>
 ids:
   - com.xingren.service:sms-client-stubs:1.5.0-SNAPSHOT:stubs:6565


最后运行 spring cloud stubrunner,即可启动 Stub 服务。前端同学就可以愉快的使用 Stub 来进行前端开发了。
DSL
Spring Cloud Contract 的契约 DSL,既可以用于生成服务方的测试,也可以用于生成供调用方使用的 Stub,但是这两种方式对数据的验证方法有一些不同。对于服务方测试,DSL 需要提供请求内容,验证响应;而对于 Stub,DSL 需要匹配请求,提供响应内容。Spring Cloud Contract 提供了几种方式来处理。
一种方式是通过 $(consumer(...), producer(...)) 的语法(或者$(stub(...), test(...))、$(client(...), server(...))、$(c(...), p(...)),都是一样的)。例如:

org.springframework.cloud.contract.spec.Contract.make {
   request {
       method('GET')
       url $(consumer(~/\/[0-9]{2}/), producer('/12'))    
      }  
   response {        
       status 200        
       body(                
           name: $(consumer('Kowalsky'), producer(regex('[a-zA-Z]+')))
       )
   }
}


上面就是指对于调用方,url 需要匹配 ~/\/[0-9]{2}/ 这个正则表达式,Stub 就会返回响应,其中 name 则为 Kowalsky。而对于服务方,生产的测试用例的请求 url 为 /12,它会验证响应中的 name 符合正则 '[a-zA-Z]+'。另外,Spring Cloud Contract 还提供了 stubMatchers 和 testMatchers 来支持更复杂的请求匹配和测试验证。
Spring Cloud Contract 现在还在快速发展中,目前对于生成测试用例的规则,还是有不够灵活的地方。例如,对于某些 Stub 应该返回,但生成的测试里不需要验证的字段,支持不太完善。还有对于 form-urlencoded 的请求,处理起来不如 Json 的请求那么方便。相信后继版本会改善。

 

总结

通过上面简单介绍,我们可以看到基于 Spring Cloud Contract 以及契约测试的方法,可以让微服务之间以及前后端之间的集成更顺畅。
另外前面还提到 Pact,它的优势是支持多种语言,但我们的环境都是基于 JVM 的,而 Spring Cloud Contract 和 SpringBoot 以及 Junit 的集成更简单方便。而且 Spring Cloud Contract 的另一个优势是它可以自动生成服务方的自动化测试。