消费者驱动合同 (CDC) 循序渐进指南:生产方持有合同

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

假设 Loan IssuanceFraud Detection 服务器的客户端。在当前冲刺中,我们必须开发一个新功能:如果客户想借太多钱,我们将其标记为欺诈。

技术说明

  • Fraud Detection 的 artifact-idhttp-server

  • Loan Issuance 的 artifact-idhttp-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 服务器的消费方)的开发者,您可能会执行以下步骤

  1. 通过为您的功能编写测试来开始 TDD。

  2. 编写缺失的实现。

  3. 在本地克隆 Fraud Detection 服务仓库。

  4. 在本地的 fraud detection 服务仓库中定义合同。

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

  6. 运行集成测试。

  7. 提交 Pull Request。

  8. 创建初始实现。

  9. 接管 Pull Request。

  10. 编写缺失的实现。

  11. 部署您的应用。

  12. 在线工作。

我们从贷款发放流程开始,如下面的 UML 图所示

getting-started-cdc-client

通过为您的功能编写测试来开始 TDD

以下清单显示了我们可能用于检查贷款金额是否过大的测试

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

编写缺失的实现

在某个时候,您需要向 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 字段的值为 FRAUDrejectionReason 字段的值为 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)中执行以下操作

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

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

  3. 使用 @AutoConfigureStubRunner 注解您的测试类。在注解中,提供 group-idartifact-id,以便 Stub Runner 下载您的协作服务的 stubs。

  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 找到了您的 stubs,并为您的应用启动了一个服务器,其 group ID 为 com.example,artifact ID 为 http-server,使用了 0.0.1-SNAPSHOT 版本的 stubs,classifier 为 stubs,端口为 8080

提交 Pull Request

您到目前为止所做的是一个迭代过程。您可以试用合同,将其安装在本地,并在消费方继续工作,直到合同按您的期望工作。

一旦您对结果感到满意并且测试通过,您可以向服务端发布 Pull Request。目前,消费方的工作已完成。

生产方 (Fraud Detection 服务器)

作为 Fraud Detection 服务器(Loan Issuance 服务的服务器)的开发者,您可能需要

  • 接管 Pull Request

  • 编写缺失的实现

  • 部署应用

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

getting-started-cdc-server

接管 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,对于 responsematchers 部分也同样适用。

请注意,在生产方,您也在进行 TDD。期望以测试的形式表达出来。该测试会向我们自己的应用发送合同中定义的 URL、头部和 body 的请求。它还期望响应中包含精确定义的值。换句话说,您处于红、绿、重构(red, green, refactor)中的 red 阶段。是时候将 red 转换为 green 了。

编写缺失的实现

因为您知道预期的输入和输出,您可以按照以下方式编写缺失的实现

当您再次运行 ./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 的命令,这将发布应用和 stub artifacts。

消费方 (Loan Issuance),最后一步

作为 loan issuance 服务(Fraud Detection 服务器的消费方)的开发者,您需要

  • 将我们的功能分支合并到 master

  • 切换到在线工作模式

以下 UML 图显示了流程的最终状态

getting-started-cdc-client-final

合并分支到 Master

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

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

在线工作

现在您可以禁用 Spring Cloud Contract Stub Runner 的离线工作模式,并指定您的 stubs 仓库的位置。此时,服务端的 stubs 会自动从 Nexus/Artifactory 下载。您可以将 stubsMode 的值设置为 REMOTE。以下代码展示了通过更改属性实现同样效果的示例

就是这样。您已完成本教程。