跳到主要内容

生产者端契约的消费者驱动契约(CDC)逐步指南

DeepSeek V3 中英对照 Step-by-step Guide to Consumer Driven Contracts (CDC) with Contracts on the Producer Side

以欺诈检测和贷款发放流程为例。业务场景是,我们希望向人们发放贷款,但又不希望他们欺骗我们。当前系统的实现是向所有人发放贷款。

假设 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

消费者端(贷款发放)​

作为贷款发放服务(欺诈检测服务器的消费者)的开发者,您可能会执行以下步骤:

  1. 通过为你的功能编写测试来开始进行测试驱动开发(TDD)。

  2. 编写缺失的实现。

  3. 在本地克隆欺诈检测服务仓库。

  4. 在欺诈检测服务仓库中本地定义契约。

  5. 添加 Spring Cloud Contract(SCC)插件。

  6. 运行集成测试。

  7. 提交拉取请求。

  8. 创建初始实现。

  9. 接管拉取请求。

  10. 编写缺失的实现。

  11. 部署你的应用程序。

  12. 在线工作。

我们从贷款发放流程开始,以下UML图展示了该流程:

getting-started-cdc-client

开始实践TDD:为你的功能编写测试​

下面的清单展示了一个测试,我们可以用它来检查贷款金额是否过大:

假设你已经为新功能编写了一个测试。如果收到一笔大额贷款申请,系统应该拒绝该贷款申请,并附带一些描述。

编写缺失的实现​

在某些时候,你需要向欺诈检测服务发送请求。假设你需要发送包含客户ID和客户想要借款金额的请求。你想通过PUT方法将其发送到/fraudcheck URL。为此,你可以使用类似以下代码:

为简化起见,欺诈检测服务的端口设置为 8080,应用程序运行在 8090 端口上。

备注

如果此时开始测试,它会失败,因为目前没有服务在端口 8080 上运行。

本地克隆欺诈检测服务仓库​

你可以从尝试服务器端合约开始。为此,首先需要克隆它,运行以下命令:

$ git clone https://your-git-server.com/server-side.git local-http-server-repo

在欺诈检测服务的仓库中本地定义合约​

作为消费者,你需要明确自己想要实现的目标。你需要制定自己的期望。为此,请编写以下合约:

important

将合约文件放置在 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)中执行以下操作:

  1. 添加 Spring Cloud Contract BOM,如下所示:

    点击此处查看代码

  2. 添加对 Spring Cloud Contract Stub Runner 的依赖,如下所示:

    点击此处查看代码

  3. 使用 @AutoConfigureStubRunner 注解你的测试类。在注解中,为 Stub Runner 提供 group-id 和 artifact-id 以下载你协作者的存根。

    点击此处查看代码

  4. (可选)由于你是在离线状态下与协作者交互,你也可以提供离线工作开关(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 已经找到了你的存根,并为你的应用程序启动了一个服务器,该服务器的存根组 ID 为 com.example,构件 ID 为 http-server,存根版本为 0.0.1-SNAPSHOT,分类器为 stubs,运行在端口 8080 上。

提交拉取请求​

到目前为止,你所做的是一个迭代过程。你可以尝试修改合约,在本地安装它,并在消费者端进行调整,直到合约按照你的期望工作。

一旦你对结果满意并且测试通过,就可以向服务器端提交一个拉取请求。目前,消费者端的工作已经完成。

生产者端(欺诈检测服务器)​

作为欺诈检测服务器(贷款发放服务的服务器)的开发者,您可能希望:

  • 接管拉取请求

  • 编写缺失的实现

  • 部署应用程序

以下UML图展示了欺诈检测流程:

getting-started-cdc-server

接管拉取请求​

作为提醒,以下清单展示了初始实现:

然后你可以运行以下命令:

$ git checkout -b contract-change-pr master
$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr

你必须添加自动生成测试所需的依赖项,如下所示:

在 Maven 插件的配置中,必须传递 packageWithBaseClasses 属性,如下所示:

important

此示例通过设置 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图展示了进程的最终状态:

getting-started-cdc-client-final

将分支合并到主分支​

以下命令展示了使用 Git 将分支合并到 master 的一种方法:

$ git checkout master
$ git merge --no-ff contract-change-pr

在线工作​

现在,你可以禁用 Spring Cloud Contract Stub Runner 的离线工作,并指定存根仓库的位置。目前,服务器端的存根会自动从 Nexus/Artifactory 下载。你可以将 stubsMode 的值设置为 REMOTE。以下代码展示了通过更改属性实现相同效果的示例:

就是这样。你已经完成了本教程。