生产者端契约的消费者驱动契约(CDC)逐步指南
以欺诈检测和贷款发放流程为例。业务场景是,我们希望向人们发放贷款,但又不希望他们欺骗我们。当前系统的实现是向所有人发放贷款。
假设 Loan Issuance 是 Fraud Detection 服务器的客户端。在当前冲刺中,我们必须开发一个新功能:如果客户想要借太多钱,我们将其标记为欺诈。
技术说明
-
欺诈检测服务的
artifact-id为http-server。 -
贷款发放服务的
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 示例仓库 中找到。
为简化起见,我们使用以下缩写:
-
Loan Issuance (LI): HTTP 客户端
-
Fraud Detection (FD): HTTP 服务器
-
SCC: Spring Cloud Contract
消费者端(贷款发放)
作为贷款发放服务(欺诈检测服务器的消费者)的开发者,您可能会执行以下步骤:
-
通过为你的功能编写测试来开始进行测试驱动开发(TDD)。
-
编写缺失的实现。
-
在本地克隆欺诈检测服务仓库。
-
在欺诈检测服务仓库中本地定义契约。
-
添加 Spring Cloud Contract(SCC)插件。
-
运行集成测试。
-
提交拉取请求。
-
创建初始实现。
-
接管拉取请求。
-
编写缺失的实现。
-
部署你的应用程序。
-
在线工作。
我们从贷款发放流程开始,以下UML图展示了该流程:

开始实践TDD:为你的功能编写测试
下面的清单展示了一个测试,我们可以用它来检查贷款金额是否过大:
假设你已经为新功能编写了一个测试。如果收到一笔大额贷款申请,系统应该拒绝该贷款申请,并附带一些描述。
编写缺失的实现
在某些时候,你需要向欺诈检测服务发送请求。假设你需要发送包含客户ID和客户想要借款金额的请求。你想通过PUT方法将其发送到/fraudcheck URL。为此,你可以使用类似以下代码:
为简化起见,欺诈检测服务的端口设置为 8080,应用程序运行在 8090 端口上。
如果此时开始测试,它会失败,因为目前没有服务在端口 8080 上运行。
本地克隆欺诈检测服务仓库
你可以从尝试服务器端合约开始。为此,首先需要克隆它,运行以下命令:
$ git clone https://your-git-server.com/server-side.git local-http-server-repo
在欺诈检测服务的仓库中本地定义合约
作为消费者,你需要明确自己想要实现的目标。你需要制定自己的期望。为此,请编写以下合约:
将合约文件放置在 src/test/resources/contracts/fraud 文件夹中。fraud 文件夹很重要,因为生产者测试基类的名称引用了该文件夹。
以下示例展示了我们的合约,分别使用 Groovy 和 YAML 编写:
YML 合约相当直观。然而,当你查看用静态类型 Groovy DSL 编写的合约时,你可能会想知道 value(client(…), server(…)) 这些部分是什么。通过使用这种表示法,Spring Cloud Contract 允许你定义 JSON 块、URL 或其他结构中的动态部分。对于标识符或时间戳等情况,你无需硬编码一个值。你希望允许一些不同的值范围。为了支持值范围,你可以设置匹配这些值的正则表达式,供消费者端使用。你可以通过映射表示法或带有插值的字符串来提供请求体。我们强烈推荐使用映射表示法。
要设置合约,你必须理解映射表示法。请参阅Groovy 关于 JSON 的文档。
之前展示的合约是双方之间的一项协议:
-
如果发送的 HTTP 请求满足以下所有条件:
-
使用
PUT方法访问/fraudcheck端点 -
JSON 请求体中
client.id匹配正则表达式[0-9]{10}且loanAmount等于99999 -
Content-Type请求头的值为application/vnd.fraud.v1+json
-
-
那么将向消费者返回 HTTP 响应,该响应:
-
状态码为
200 -
包含 JSON 响应体,其中
fraudCheckStatus字段的值为FRAUD,rejectionReason字段的值为Amount too high -
Content-Type响应头的值为application/vnd.fraud.v1+json
-
准备好在实际的集成测试中检查API时,您需要在本地安装存根。
添加 Spring Cloud Contract Verifier 插件
我们可以添加 Maven 或 Gradle 插件。在本示例中,我们将展示如何添加 Maven。首先,我们添加 Spring Cloud Contract BOM,如下例所示:
接下来,添加 Spring Cloud Contract Verifier Maven 插件,如下例所示:
添加插件后,您将获得 Spring Cloud Contract Verifier 功能,该功能基于提供的契约:
-
生成并运行测试
-
生成并安装存根
您不希望生成测试,因为作为使用者,您只想使用存根。您需要跳过测试的生成和调用。为此,请运行以下命令:
$ 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 的存根已安装在本地仓库中。
运行集成测试
为了利用 Spring Cloud Contract Stub Runner 自动下载存根的功能,您必须在消费者端项目(Loan Application service)中执行以下操作:
现在,当你运行测试时,你会在日志中看到类似以下的输出:
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 已经找到了你的存根,并为你的应用程序启动了一个服务器,该服务器的存根组 ID 为 com.example,构件 ID 为 http-server,存根版本为 0.0.1-SNAPSHOT,分类器为 stubs,运行在端口 8080 上。
提交拉取请求
到目前为止,你所做的是一个迭代过程。你可以尝试修改合约,在本地安装它,并在消费者端进行调整,直到合约按照你的期望工作。
一旦你对结果满意并且测试通过,就可以向服务器端提交一个拉取请求。目前,消费者端的工作已经完成。
生产者端(欺诈检测服务器)
作为欺诈检测服务器(贷款发放服务的服务器)的开发者,您可能希望:
-
接管拉取请求
-
编写缺失的实现
-
部署应用程序
以下UML图展示了欺诈检测流程:

接管拉取请求
作为提醒,以下清单展示了初始实现:
然后你可以运行以下命令:
$ 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 上下文或任何必要的配置。在这种情况下,你应该使用 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、头部和正文均在契约中定义。同时,它期望在响应中获得精确定义的值。换句话说,你已经完成了红、绿、重构中的红部分。现在是时候将红转变为绿了。
编写缺失的实现
由于你已知预期的输入和输出,你可以按如下方式编写缺失的实现:
当你再次运行 ./mvnw clean install 时,测试将通过。由于 Spring Cloud Contract Verifier 插件将测试添加到了 generated-test-sources 中,你实际上可以从你的 IDE 中运行这些测试。
部署您的应用程序
完成工作后,您可以部署您的更改。为此,您必须首先通过运行以下命令来合并分支:
$ git checkout master
$ git merge --no-ff contract-change-pr
$ git push origin master
您的 CI 可能会运行类似 ./mvnw clean deploy 的命令,该命令将同时发布应用程序和存根构件。
消费者端(贷款发放),最终步骤
作为贷款发放服务的开发者(欺诈检测服务器的消费者),你需要:
-
将我们的功能分支合并到
master -
切换到在线工作模式
以下UML图展示了进程的最终状态:

将分支合并到主分支
以下命令展示了使用 Git 将分支合并到 master 的一种方法:
$ git checkout master
$ git merge --no-ff contract-change-pr
在线工作
现在,你可以禁用 Spring Cloud Contract Stub Runner 的离线工作,并指定存根仓库的位置。目前,服务器端的存根会自动从 Nexus/Artifactory 下载。你可以将 stubsMode 的值设置为 REMOTE。以下代码展示了通过更改属性实现相同效果的示例:
就是这样。你已经完成了本教程。