前言
上一篇文章破局微服务架构演化——初探消费者驱动契约提到了微服务测试问题、微服务架构演化的难点,并介绍了消费者驱动契约对于这些问题的解决方案。其实消费者驱动契约原理非常简单,大白话就是 —— 消费方 向 提供方 提需求,并且把需求写入合同,提供方 根据合同满足 消费方 的需求,Consumer 拿着合同对结果进行验收。
接下来这篇文章会模拟一个简单的业务场景,通过使用 Spring Cloud Contract(一个消费者驱动契约测试框架/提供者驱动契约测试框架)来演示消费者驱动契约测试的实现和原理。
文章能带来哪些收获
- 了解 Spring Cloud Contract 消费者驱动契约测试的使用和基本原理。
- 了解测试先行。
- 了解如何基于 git 管理契约。
- 了解服务提供者如何集成 Spring Cloud 契约测试、Spring REST Docs 以及集成测试框架 WebTestClient,并自动生成 API 文档。
笔者写着写着,发现一篇文章要涵盖所有细节,会导致文章太过臃肿,笔者自己都读不下去了,所以删了很多篇幅,后续再按主题拆开写,这篇文章需要读者去看代码,根据 README.md 的描述跑起来,不然并不能带来什么收获。
Spring Cloud Contract 简介
Spring Cloud Contract 的前身是 Accurest,Accurest 是一个消费者契约测试框架,这个项目诞生于 2014 年,后来移到 Spring Cloud 下并改名为 Spring Cloud Contract,然后 Accurest 就停止维护。
Spring Cloud Contract 是一个消费者驱动契约测试框架/提供者驱动契约测试框架,它的诞生主要解决微服务中端对端测试难题。它通过契约 + 测试 + Stub Server 简化端对端测试的复杂性,既保留了端对端测试中服务之间真实的通信,又保留了集成测试/单元测试快速反馈的优点。
Spring Cloud Contract 基本功能
Spring Cloud Contract 的核心功能是支持契约DSL、HTTP(S) 通信协议和消息通信协议,当然 Spring Cloud Contract 还集成了很多其它流行的协议和工具,下面列出一小部分功能,具体可以看看官方文档:
- Contract DSL
- Contract for HTTP(s)
- Contract for Messaging
- Integration GraphQL
- Integration GRPC
- Integration REST Docs
实现消费者驱动契约测试
Spring Cloud Contract 完全基于测试驱动开发的理念,因此我的实现过程会尽量贴合这个理念,至于测试驱动开发是什么笔者就不多说了,感兴趣的读者可以去看看笔者之前写的一篇文章 测试驱动开发(TDD)总结——原理篇。
前置
- 准备 Github 账号,并配置 SSH Key
- Fork contract consumer-driven-contract-git
- Fork privoder repository fraud
- Fork consumer repository loan
- 生成 Github Access Token
- 一些开发相关的配置参考 README.md
需求
假设我们需要为xxx企业开发一款金融贷款产品,该产品现阶段需要支持贷款业务。与此同时,我们还需要对借贷过程进行业务防控,对贷款人进行贷前检查,识别高危用户申请、机构代办、多头借贷、组团骗贷等互联网金融风险,帮助客户提升反欺诈识别能力。
业务模型
graph LR
贷款 --> 反欺诈
验收标准
场景:贷款成功
Given 借款人(id -> 123456789) 贷款 99999 RMB
And 借款人符合贷款要求
When 借款人发起贷款申请
Then 贷款成功
场景:贷款金额太大,贷款失败
Given 借款人(id -> 987654321) 贷款 999999999 RMB
When 借款人发起贷款申请
Then 贷款失败
And 提示:贷款金额太高
协商契约
假设通过协商,给出以下两份契约
1. 贷款人通过反欺诈检测
description: |
Borrowers pass anti-fraud checks
```
Given borrower(id -> 123456789) and loan 99999 RMB
And Borrower meet loan requirements
When Conduct anti-fraud checks on borrower
Then Borrower pass anti-fraud checks
```
request: # (1)
method: PUT # (2)
url: /fraudcheck # (3)
body: # (4)
"client.id": 1234567890
loanAmount: 99999
headers: # (5)
Content-Type: application/json
matchers:
body:
- path: $.['client.id'] # (6)
type: by_regex
value: "[0-9]{10}"
response: # (7)
status: 200 # (8)
body: # (9)
fraudCheckStatus: "NO_FRAUD"
headers: # (10)
Content-Type: application/json
2. 贷款人贷款金额太高,没有通过反欺诈检测
description: |
The borrower failed an anti-fraud check because loan amount too high
```
Given borrower(id -> 9876543210) loan 999999999 RMB
When Conduct anti-fraud checks on borrower
And loan amount too high
Then The borrower failed an anti-fraud checks
And response injection reason of Amount too high
```
request: # (1)
method: PUT # (2)
url: /fraudcheck # (3)
body: # (4)
"client.id": 9876543210
loanAmount: 999999999
headers: # (5)
Content-Type: application/json
matchers:
body:
- path: $.['client.id'] # (6)
type: by_regex
value: "[0-9]{10}"
response: # (7)
status: 200 # (8)
body: # (9)
fraudCheckStatus: "FRAUD"
"rejection.reason": "Amount too high"
headers: # (10)
Content-Type: application/json
上传契约到契约仓库
基于 git 的契约仓库的目录有一定的规范,在实现的时候需要遵守目录规范,具体规范可以看看官方文档。
接下来这里没什么好说的,使用 git push
命令将契约推到代码仓库即可
提供方(Fraud)
测试先行
服务提供方的契约测试是由 Spring Cloud Contract 根据契约自动生成,只需要在项目根目录下执行./gradlew clean contractTest
命令,就可以在 build/generated-test-sources
目录下看到生成的契约测试代码,生成的代码长这样:
由于没有写实现代码,所以契约测试执行失败,有两种方式可以通过契约测试:
- Mock
xxxService.xxx()
返回契约中定义的返回值。 - 编写实现代码。
这个阶段个人建议选择第一种,主要的原因是让消费方能够更早跟 Stub Server 集成,而不是等待 Provider 编写实现代码再集成。
提交 Stub mappings 到契约仓库
契约测试通过后 可通过 ./gradlew clean publishStubsToScm
将 stub mappings json 文件提交到契约仓库。执行成功会在 build/stubs/mappings
目录下生成 stub mappings json 文件,stub server 正是通过导入这些 json 文件来生成契约接口,才能向消费方提供访问服务。
代码实现
文章篇幅有限,读者可以上 github 看代码
消费方(Loan)
测试先行
@Test
void should_loan_successfully() {
// given:
var givenLoan = new LoanCommand(1234567890L, 99999L);
// when:
WebTestClient.ResponseSpec responseSpec = webTestClient.post()
.uri("/loans")
.body(BodyInserters.fromValue(givenLoan))
.exchange();
// then:
responseSpec.expectStatus().is2xxSuccessful()
.expectBody().isEmpty();
}
@Test
void should_loan_failure_when_amount_too_high() {
// given:
var givenLoan = new LoanCommand(9876543210L, 999999999L);
// when:
WebTestClient.ResponseSpec responseSpec = webTestClient.post()
.uri("/loans")
.body(BodyInserters.fromValue(givenLoan))
.exchange();
// then:
responseSpec
.expectStatus().is4xxClientError()
.expectBody()
.jsonPath("$.message", is("贷款金额太高"));
}
代码实现
文章篇幅有限,读者可以上 github 看代码
端对端测试
当进行端对端测试时,下面展示了服务与 stub server 之间的关系:
实现原理
笔者裁剪了很多细节,流程可以看图,我主要讲引入的一些重要组件:
-
Contracts Repository:Contracts Repository 的实现不只是用 git,还可以使用 maven 仓库等,它的职责是统一管理 Contract,确保 Contract 变更后能够及时同步,也能确保 Consumer 和 Provider 能够拉取到同一份 Contract,以便于双方能够对同一份 Contract 达成承诺。
-
Stub Server:Spring Cloud Contract 使用 WireMock 实现 Stub Server,Stub Server 可以理解为是 Provider 的替身,它的职责是模拟 Provider 接收和响应 Consumer 发起的请求,请求和响应都是 Contract 中明确定义的。
官网提供了一些更丰富的实现原理流程图,读者感兴趣可以去看看。
基于 SCC 测试的优缺点
优点
- Provider 侧的契约测试由 SCC plugin 根据契约自动化生成,确保 Provider 对契约的承诺。
- Provider 侧的契约测试一定程度上可以替代集成测试,当作端对端测试来用。
- Provider 侧的契约测试集成 Spring REST Docs 可以自动化生成 API 文档。
- Consumer 侧做端对端测试较为友好,可以给开发者更多信心。
- 契约仓库的实现可以多种选择,可以基于 git、自建 maven 仓库和本地 maven 仓库等。
- 支持多种协议
缺点
- 实现原理花哨,细节多,配置多,集成起来没有很方便,主要集中在 Provider 侧。
- 前端集成 Spring Cloud Contract 不友好,需要基于 docker 来实现(基于 docker 还算能接受一点)。
总结
文章通过一个简单的“需求”来演示如何使用 Spring Cloud Contract 实现消费者驱动契约测试,在实现的过程中了解如何定义契约,了解测试优先原则,了解契约测试,并对实现原理做了简单的讲解,最后对基于 SCC 测试的优缺点进行了一些评价。文章省略了很多细节,最后希望读者能把代码拉下来走一篇流程感受一下消费者驱动契约测试。
后续
后续笔者的想法是研究 Spring Cloud Contract 如何跟前端集成,让前端也能体验消费者驱动契测试的优点,尽情期待。