CDC 之 Spring Cloud Contract 看完就会用

先上个导图


CDC 之 Spring Cloud Contract 看完就会用_第1张图片
Spring Cloud Contract

一 什么是 CDC


消费者驱动的契约测试(Consumer-Driven Contracts,简称CDC),是指从消费者业务实现的角度出发,驱动出契约,再基于契约,对提供者验证的一种测试方式。

通常在开发过程中主要是由服务提供方来提供约定接口,虽然提供方架构和接口进行调整后会通知消费者,但还是会存在风险的。而在微服务架构中,不同的服务可能会由不同的团队维护,这种情况下对接口的开发和维护也将会带来一些问题。

为了解决这些问题呢,Ian Robinson提出了一个以服务消费者定义契约为驱动的开发模式:“Consumer-Driver Contracts(CDC)”,就是:消费者驱动契约。而 CDC 的总体流程是,消费者定义了它们期望的 API/消息 是什么样子,这种期望就被称为契约,提供者需要编写验证这些契约并生成 stubs 供生产者重复使用。

Spring Cloud Contract 则是 CDC 的一种具体实现。

二 微服务架构下测试存在的一些问题


CDC 之 Spring Cloud Contract 看完就会用_第2张图片
multiple microservices

假设我们有一个由多个 services 组成的系统,我们在没有 CDC 的情况下对 v1 这个 service 进行测试:

  1. 对于端到端的测试
  • 为了测试一个 service,我们需要将所有的 services 和对应的 databases 都部署起来,一旦我们有很多的 service 后,这个成本非常大
  1. 对于单元/集成测试
  • 在编写测试时候需要 mock 大量的数据
  • 很难应对服务端调整架构或接口后带来的问题

三 Spring Cloud Contract 的解决方案


Spring Cloud Contract 可以为我们生成一个可被验证的 Stub Runner,这样我们就可以在不启动其它 services 的同时及时的获取反馈信息,因为在双方都准守契约的情况下这个 Stub Runner 就相当于我们启动了对应的 service

CDC 之 Spring Cloud Contract 看完就会用_第3张图片
stubs

好处:

  1. 确保 WireMock/message stubs 是完全按照实际的服务端实现的
  2. 推广 ATDD 方法和为服务架构
  3. 当提供者发布最新的契约时,双方都可以快速发现
  4. 既可以对自身服务进行测试,也可以生成 stub 提供给其它 service 进行调用

四 Quickly Start


server side

Spring Cloud Contract 采用 Groovy DSL 来定义契约,当然了你也可以采用 yml 来编写。现在我们来实现一个简单的 stub ,包含两个 API,根据 id 获取用户信息以及添加一条用户信息

1 编写 contracts

add user


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

Contract.make {
    name 'should_return_status_created' //最终生成测试时的方法名,默认是文件名
    request {
        method 'post'
        url '/users'
        body([
                id      : 1,
                username: 'acey'
        ])
        headers {
            contentType(applicationJsonUtf8())
        }
    }
    response {
        status 201
    }
}

get user

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

Contract.make {
    name 'should_return_user'  //最终生成测试时的方法名,默认是文件名
    request {
        method 'GET'
        url value(
                consumer(regex('/users/\\d+')), 
                producer('/users/1'))
    }
    response {
        status 200
        body([
                id      : 1L,
                username: "acey",
        ])
        testMatchers {
            jsonPath('$.id', byRegex(number()))
            jsonPath('$.username',byRegex(onlyAlphaUnicode()))
        }
        headers {
            contentType(applicationJsonUtf8())
        }
    }
}

其中:

  url value(
                consumer(regex('/users/\\d+')), 
                producer('/users/1'))

当生成 stub 后,

  • consumer 表示客户端在发送该请求时可以进行正则匹配,只需要保证
    users 后跟的是一个数字即可
  • producer 表示当客服端发送了符合正则的请求后,所以符合的请求都将调用 /users/1 这个接口

而对于 response 也是类似

response {
        status 200
        body([
                id      : 1L,
                username: "acey",
        ])
        testMatchers {
            jsonPath('$.id', byRegex(number()))
            jsonPath('$.username',byRegex(onlyAlphaUnicode()))
        }
        headers {
            contentType(applicationJsonUtf8())
        }
    }

body 里面的数据是客户端在请求后返回的具体的结果
testMatchers 则是针对服务端在自身测试时进行一个模糊验证

2 生成/运行测试

使用 ./gradlew generateContractTests 生成 contracts 对应的测试,当然你也可以直接 build / test 。生成的测试长这样


public class UserTest extends UserBase {

    @Rule
    public Base base = new Base();

    @Test
    public void validate_should_return_status_created() throws Exception {
        // given:
            MockMvcRequestSpecification request = given()
                    .header("Content-Type", "application/json;charset=UTF-8")
                    .body("{\"id\":1,\"username\":\"acey\"}");

        // when:
            ResponseOptions response = given().spec(request)
                    .post("/users");

        // then:
            assertThat(response.statusCode()).isEqualTo(201);
    }

    @Test
    public void validate_should_return_user() throws Exception {
        // given:
            MockMvcRequestSpecification request = given();

        // when:
            ResponseOptions response = given().spec(request)
                    .get("/users/1");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).matches("application/json;charset=UTF-8.*");
        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        // and:
            assertThat(parsedJson.read("$.id", String.class)).matches("-?\\d*(\\.\\d+)?");
            assertThat(parsedJson.read("$.username", String.class)).matches("[\\p{L}]*");
    }
}

有必要再来看一下目录结构


CDC 之 Spring Cloud Contract 看完就会用_第4张图片
root path

更多配置信息

3 生成 stub

执行 ./gradlew install 会在本机的 .m2/repository 下生成一个 **-stub.jar
当然你也可以发布发布到一个 remote repository

client side

当有了 stub 之后,在 client side 去调用就比较简单了,在这就只演示下 get user 这个 API (当然你也可以采用契约测试进行测试)

UserServiceTest.class

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner(ids = "com.acey:server:+:stubs:10001", 
      workOffline = true)
@ActiveProfiles("test")
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void getUserInfo() throws Exception {
        ResponseEntity userInfo = userService.getUserInfo(1L);
        Map user = (Map) userInfo.getBody();
        assertEquals(HttpStatus.OK, userInfo.getStatusCode());
        assertEquals("acey", user.get("username"));
    }
}

其中

com.acey:server:+:stubs:10001

运行的时候会在我们本地 `.m2/repository` 目录下去找 com.acey:server:+:stubs 这个这个 jar 文件
并 run 起来,端口是 10001,`+` 表示会自动找最新的版本

UserService.class


@Service
public class UserService {
    @Value("${userCenter}")
    private String userCenterUrl;

    public ResponseEntity getUserInfo(Long userId) {
        String getUsersUrl = userCenterUrl + "/users/" + userId;

        RestTemplate template = new RestTemplate();
        ResponseEntity result = template.getForEntity(getUsersUrl, Map.class);
        return result;
    }
}

application-test.yml

userCenter: http://localhost:10001

五 独立部署

1 首先下载stub-runner.jar

wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.1.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.1.1.RELEASE.jar'

2. 在同路径创建配置文件application.yml,并添加stubrunner的配置,如下

stubrunner:
  ids:
    # - [groupId]:artifactId:[version]:[classify]:[port]
    # 指定运行在8081端口
    - com.jtj.cloud:spring-contract-example:1.0.0:+:8081
  # 该模式下会从远程maven仓库缓存到本地
  stubs-mode: local[remote 每次拉取远程]

3. 通过java -jar stub-runner.jar运行,访问对应端口请求数据

完整 Demo 地址

参考:

  • https://cloud.spring.io/spring-cloud-contract/multi/multi_spring-cloud-contract.html
  • http://blog.didispace.com/spring-cloud-contract-summary-hzf/
  • http://www.infoq.com/cn/news/2017/04/spring-cloud-contract

你可能感兴趣的:(CDC 之 Spring Cloud Contract 看完就会用)