消费者驱动合同 (CDC) 循序渐进指南:生产方持有合同
考虑一个欺诈检测和贷款发放流程的例子。业务场景是我们希望向人们发放贷款,但又不希望他们欺诈我们。我们系统当前的实现会向所有人发放贷款。
假设 Loan Issuance
是 Fraud Detection
服务器的客户端。在当前冲刺中,我们必须开发一个新功能:如果客户想借太多钱,我们将其标记为欺诈。
技术说明
-
Fraud Detection 的
artifact-id
为http-server
。 -
Loan Issuance 的
artifact-id
为http-client
。 -
两者的
group-id
均为com.example
。 -
为了本例的需要,
Stub Storage
为 Nexus/Artifactory。
社交说明
-
客户端和服务端开发团队都需要在整个流程中直接沟通并讨论变更。
-
CDC 的核心在于沟通。
服务端代码位于 Spring Cloud Contract Samples 仓库的 samples/standalone/dsl/http-server
路径下,客户端代码位于 Spring Cloud Contract 仓库的 samples/standalone/dsl/http-client
路径下。
在本例中,生产方拥有合同。实际上,所有合同都位于生产方的仓库中。 |
技术说明
重要提示:所有代码均可在 Spring Cloud Contract Samples 仓库中找到。
为简单起见,我们使用以下缩写
-
Loan Issuance (LI): HTTP 客户端
-
Fraud Detection (FD): HTTP 服务器
-
SCC: Spring Cloud Contract
消费方 (Loan Issuance)
作为 Loan Issuance 服务(Fraud Detection 服务器的消费方)的开发者,您可能会执行以下步骤
-
通过为您的功能编写测试来开始 TDD。
-
编写缺失的实现。
-
在本地克隆 Fraud Detection 服务仓库。
-
在本地的 fraud detection 服务仓库中定义合同。
-
添加 Spring Cloud Contract (SCC) 插件。
-
运行集成测试。
-
提交 Pull Request。
-
创建初始实现。
-
接管 Pull Request。
-
编写缺失的实现。
-
部署您的应用。
-
在线工作。
我们从贷款发放流程开始,如下面的 UML 图所示

编写缺失的实现
在某个时候,您需要向 Fraud Detection 服务发送请求。假设您需要发送包含客户 ID 和客户希望借款金额的请求。您希望使用 PUT
方法将其发送到 /fraudcheck
URL。为此,您可以使用类似如下的代码
为简单起见,Fraud Detection 服务的端口设置为 8080
,应用运行在 8090
端口。
如果您此时运行测试,它会失败,因为当前没有服务运行在 8080 端口上。 |
在本地克隆 Fraud Detection 服务仓库
您可以从试用服务端合同开始。为此,您必须首先通过运行以下命令克隆它
$ git clone https://your-git-server.com/server-side.git local-http-server-repo
在本地的 Fraud Detection 服务仓库中定义合同
作为消费方,您需要定义您究竟想要实现什么。您需要明确您的期望。为此,编写以下合同
将合同放在 src/test/resources/contracts/fraud 文件夹中。fraud 文件夹很重要,因为生产方的测试基类名称会引用该文件夹。 |
以下示例显示了我们的合同,包括 Groovy 和 YAML 格式
YML 合同非常直观。然而,当您查看使用静态类型 Groovy DSL 编写的合同时,您可能会想知道 value(client(…), server(…))
部分是什么。通过使用这种表示法,Spring Cloud Contract 允许您定义 JSON 块、URL 或其他结构的动态部分。在标识符或时间戳的情况下,您无需硬编码一个值。您希望允许不同的值范围。要启用值范围,您可以为消费方设置匹配这些值的正则表达式。您可以通过 map 表示法或带插值的 String 来提供 body。我们强烈建议使用 map 表示法。
要设置合同,您必须理解 map 表示法。请参阅 Groovy 关于 JSON 的文档。 |
之前显示的合同是双方之间的协议,内容如下
-
如果发送一个 HTTP 请求,包含以下所有内容
-
在
/fraudcheck
端点上使用PUT
方法 -
一个 JSON body,其中
client.id
匹配正则表达式[0-9]{10}
,且loanAmount
等于99999
-
一个
Content-Type
头部,其值为application/vnd.fraud.v1+json
-
-
则会向消费方发送一个 HTTP 响应,该响应
-
状态为
200
-
包含一个 JSON body,其中
fraudCheckStatus
字段的值为FRAUD
,rejectionReason
字段的值为Amount too high
-
包含一个
Content-Type
头部,其值为application/vnd.fraud.v1+json
-
一旦您准备好在集成测试中实际检查 API,您需要在本地安装 stubs。
添加 Spring Cloud Contract Verifier 插件
我们可以添加 Maven 或 Gradle 插件。在本例中,我们展示如何添加 Maven。首先,我们添加 Spring Cloud Contract
BOM,如下例所示
接下来,添加 Spring Cloud Contract Verifier
Maven 插件,如下例所示
由于添加了插件,您将获得 Spring Cloud Contract Verifier
功能,这些功能会根据提供的合同
-
生成并运行测试
-
生成并安装 stubs
您不需要生成测试,因为作为消费方,您只需要使用 stubs。您需要跳过测试的生成和调用。为此,请运行以下命令
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests
运行这些命令后,您应该会在日志中看到类似以下内容
[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
以下这行非常重要
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
它确认 http-server
的 stubs 已经安装到本地仓库。
运行集成测试
为了利用 Spring Cloud Contract Stub Runner 的自动下载 stub 功能,您必须在您的消费方项目(Loan Application service
)中执行以下操作
-
添加
Spring Cloud Contract
BOM,如下所示 -
添加对
Spring Cloud Contract Stub Runner
的依赖,如下所示 -
使用
@AutoConfigureStubRunner
注解您的测试类。在注解中,提供group-id
和artifact-id
,以便 Stub Runner 下载您的协作服务的 stubs。 -
(可选)由于您是在离线状态下使用协作服务,您也可以提供离线工作开关(
StubRunnerProperties.StubsMode.LOCAL
)。
现在,当您运行测试时,您会在日志中看到类似以下输出
2016-07-19 14:22:25.403 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is + - will try to resolve the latest version
2016-07-19 14:22:25.438 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is 0.0.1-SNAPSHOT
2016-07-19 14:22:25.439 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
2016-07-19 14:22:25.451 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
2016-07-19 14:22:25.465 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
2016-07-19 14:22:25.475 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
2016-07-19 14:22:27.737 INFO 41050 --- [ main] o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]
此输出意味着 Stub Runner 找到了您的 stubs,并为您的应用启动了一个服务器,其 group ID 为 com.example
,artifact ID 为 http-server
,使用了 0.0.1-SNAPSHOT
版本的 stubs,classifier 为 stubs
,端口为 8080
。
生产方 (Fraud Detection 服务器)
作为 Fraud Detection 服务器(Loan Issuance 服务的服务器)的开发者,您可能需要
-
接管 Pull Request
-
编写缺失的实现
-
部署应用
以下 UML 图显示了欺诈检测流程

接管 Pull Request
提醒一下,以下清单显示了初始实现
然后您可以运行以下命令
$ git checkout -b contract-change-pr master
$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
您必须添加自动生成测试所需的依赖项,如下所示
在 Maven 插件的配置中,您必须传递 packageWithBaseClasses
属性,如下所示
本例通过设置 packageWithBaseClasses 属性使用“基于约定”的命名。这样做意味着最后两个包组合起来构成测试基类的名称。在本例中,合同位于 src/test/resources/contracts/fraud 下。由于从 contracts 文件夹开始没有两个包,因此只选取一个,即 fraud 。添加 Base 后缀并将 fraud 首字母大写。这样就得到了 FraudBase 测试类名称。 |
所有生成的测试都扩展该类。在那里,您可以设置您的 Spring Context 或任何必要的内容。在本例中,您应该使用 Rest Assured MVC 来启动服务端 FraudDetectionController
。以下清单显示了 FraudBase
类
现在,如果您运行 ./mvnw clean install
,您将看到类似以下输出
Results :
Tests in error:
ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...
此错误发生是因为您有一个新的合同,并从中生成了测试,由于您尚未实现该功能,测试失败了。自动生成的测试将看起来像以下测试方法
@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/vnd.fraud.v1+json")
.body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
}
如果您使用了 Groovy DSL,您可以看到合同中所有存在于 value(consumer(…), producer(…))
块中的 producer()
部分都被注入到了测试中。如果您使用 YAML,对于 response
的 matchers
部分也同样适用。
请注意,在生产方,您也在进行 TDD。期望以测试的形式表达出来。该测试会向我们自己的应用发送合同中定义的 URL、头部和 body 的请求。它还期望响应中包含精确定义的值。换句话说,您处于红、绿、重构(red
, green
, refactor
)中的 red
阶段。是时候将 red
转换为 green
了。
消费方 (Loan Issuance),最后一步
作为 loan issuance 服务(Fraud Detection 服务器的消费方)的开发者,您需要
-
将我们的功能分支合并到
master
-
切换到在线工作模式
以下 UML 图显示了流程的最终状态

合并分支到 Master
以下命令展示了使用 Git 将分支合并到 master 的一种方法
$ git checkout master
$ git merge --no-ff contract-change-pr