Step-by-step Guide to Consumer Driven Contracts (CDC) with Contracts on the Producer Side

考虑欺诈检测和贷款发放流程的示例。业务场景是这样,我们想要向人们发放贷款,但不想让他们在我们这里偷窃。我们系统的当前实现向每个人发放贷款。 假设 Loan IssuanceFraud Detection 服务器的客户端。在当前 sprint 中,我们必须开发新功能:如果客户端想要借贷太多钱,我们将客户端标记为欺诈。 技术意见

  • 欺诈检测的`artifact-id`=http-server

  • 贷款发放的`artifact-id`=http-client

  • 两者都有`group-id`=com.example

  • 为了这个示例,“存根存储”是 Nexus/Artifactory。

社会意见

  • 客户端和服务器开发团队都需要在整个过程中直接沟通并讨论变更。

  • CDC 完全是关于沟通的。

服务器端代码在 Spring Cloud Contract 的资源库 samples/standalone/dsl/http-server 路径下提供,客户端代码在 Spring Cloud Contract 的资源库 samples/standalone/dsl/http-client 路径下提供。

这种情况下,生产者拥有合同。从物理上讲,所有合同都在生产者存储库中。

Technical Note

如果你使用 SNAPSHOT、Milestone 或候选版本,你需要将以下部分添加到你的 build:

  • Maven

link:{samples_path}/standalone/dsl/http-server/pom.xml[role=include]
Gradle
link:{samples_path}/standalone/dsl/http-server/build.gradle[role=include]

为简便起见,我们使用以下首字母缩写词:

  • 贷款发放 (LI):HTTP 客户端

  • 欺诈检测 (FD):HTTP 服务器

  • SCC: Spring Cloud Contract

The Consumer Side (Loan Issuance)

作为贷款发放服务(欺诈检测服务器的使用者)的开发者,你可能会执行如下步骤:

  1. 通过为你的功能编写测试开始进行 TDD。

  2. Write the missing implementation.

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

  4. 在欺诈检测服务的存储库中在本地定义合同。

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

  6. Run the integration tests.

  7. File a pull request.

  8. Create an initial implementation.

  9. 接管合并请求。

  10. Write the missing implementation.

  11. Deploy your application.

  12. Work online.

我们首先从贷款发放流程开始,如下 UML 图所示:

"Loan\nIssuance"->"Loan\nIssuance": start doing TDD\nby writing a test\nfor your feature
"Loan\nIssuance"->"Loan\nIssuance": write the \nmissing implementation
"Loan\nIssuance"->"Loan\nIssuance": run a test - it fails\ndue to no server running
"Loan\nIssuance"->"Fraud\nDetection\nClone": clone the repository
"Fraud\nDetection\nClone"->"Fraud\nDetection\nClone": add missing dependencies\n& define contracts
"Fraud\nDetection\nClone"->"Fraud\nDetection\nClone": add the SCC plugin
"Fraud\nDetection\nClone"->"FD \nClone Build": install the stubs locally
"FD \nClone Build"->"SCC Plugin \nin FD Clone": generate stubs \nand stubs \nartifact (e.g. stubs-jar)
"SCC Plugin \nin FD Clone"->"FD \nClone Build": stubs and artifacts\ngenerated
"FD \nClone Build"->"Local storage": install the stubs locally
"Local storage"->"FD \nClone Build": stub sucessfully installed
"FD \nClone Build"->"Fraud\nDetection\nClone": build successful
"Loan\nIssuance"->"Loan\nIssuance": add a SCC\nStub Runner\ndependency\nand setup
"Loan\nIssuance"->"LI\nSCC\nStub Runner": start stubs\nof FD from\nlocal storage
"LI\nSCC\nStub Runner"->"Local storage": find stubs of [FD]
"Local storage"->"LI\nSCC\nStub Runner": stubs of [FD] found
"LI\nSCC\nStub Runner"->"FD stub": run stubs of [FD]
"FD stub"->"LI\nSCC\nStub Runner": [FD] stub is running
"LI\nSCC\nStub Runner"->"Loan\nIssuance": stubs running and ready for the test
"Loan\nIssuance"->"Loan\nIssuance": run a test
"Loan\nIssuance"->"FD stub": the test\nsends a request\nto the running stub
"FD stub"->"Loan\nIssuance": stub responds successfuly
"Loan\nIssuance"->"Loan\nIssuance": the test passes successfully
"Loan\nIssuance"->"Fraud\nDetection": send a pull request\nwith the\nsuggested contracts

Start Doing TDD by Writing a Test for Your Feature

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

link:{samples_path}/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[role=include]

假设你已编写了一个新功能测试。如果收到大额贷款申请,系统应拒绝该贷款申请并提供一些说明。

Write the Missing Implementation

在某些时间点,你需要向欺诈检测服务发送一个请求。假设你需要发送一个请求,其中包含客户 ID 和客户希望借贷的金额。你想发送到 /fraudcheck URL,并使用 PUT 方法。为此,你可能会使用类似于以下的代码:

link:{samples_path}/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java[role=include]

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

如果您在此时开始测试,将会出现故障,因为没有服务当前在端口`8080`上运行。

Clone the Fraud Detection service repository locally

你可以通过捣鼓服务器端合约来开始。为此,你必须首先克隆它,方法如下,运行以下命令:

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

Define the Contract Locally in the Repository of the Fraud Detection Service

作为使用者,你需要定义你具体希望实现什么。你需要制定你的期望。为此,编写以下合约:

将合同放置在`src/test/resources/contracts/fraud`文件夹中。`fraud`文件夹非常重要,因为生产者的测试基础类名称引用了该文件夹。

以下示例展示了我们的合约,既有 Groovy,也有 YAML:

link:{samples_path}/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[role=include]
yaml
link:{samples_path}/standalone/dsl/http-server/src/test/resources/contracts/yml/fraud/shouldMarkClientAsFraud.yml[role=include]

YML 合约非常简单明了。不过,当你看下使用静态类型 Groovy DSL 编写的合约时,你可能会想知道`value(client(…​), server(…​))` 部分是什么。通过使用此符号,Spring Cloud Contract 允许你定义 JSON 块、URL 或其他动态结构的部分。对于标识符或时间戳,你不用硬编码一个值。你想允许一些不同的值范围。为了启用值范围,你可以设置匹配使用者端那些值的正则表达式。你可以使用映射符号或使用带有内插机制的字符串提供主体。我们强烈推荐使用映射符号。

要设置契约,您必须了解映射表示法。请参见 Groovy docs regarding JSON

先前展示的合约是双方之间的协议:

  • 如果发送包含以下所有内容的 HTTP 请求:

    • /fraudcheck 终端上的`PUT` 方法

    • 具有与正则表达式 [0-9]{10} 匹配的 client.id,且 loanAmount 等于 99999 的 JSON 正文

    • 一个包含`application/vnd.fraud.v1+json` 值的`Content-Type` 标头

  • 然后向消费者发送 HTTP 响应

    • Has status 200

    • 包含一个 JSON 正文,其中包含一个包含`FRAUD` 值的`fraudCheckStatus` 域,并且`rejectionReason` 域的值为`Amount too high`

    • 具有 @{1} 标头和 @{2} 值

一旦你准备好实际在集成测试中检查 API,你需要在本地安装存根。

Add the Spring Cloud Contract Verifier Plugin

我们可以添加 Maven 插件或 Gradle 插件。在本示例中,我们展示如何添加 Maven。首先,我们添加 Spring Cloud Contract BOM,如下例所示:

link:{samples_path}/standalone/dsl/http-server/pom.xml[role=include]

然后,添加 Spring Cloud Contract Verifier Maven 插件,如下例所示:

link:{samples_path}/standalone/dsl/http-server/pom.xml[role=include]

由于已添加插件,你可以获得 Spring Cloud Contract Verifier 功能,这些功能从提供的合约中获取:

  • Generate and run tests

  • Produce and install 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 的存根已安装在本地存储库中。

Running the Integration Tests

要从 Spring Cloud Contract Stub Runner 中获取自动存根下载功能的收益,您必须在消费者端项目 (LoanApplication service) 中执行以下操作:

  1. 添加 @{3} BOM,如下所示:[source, xml]

link:{samples_path}/standalone/dsl/http-client/pom.xml[role=include]
  1. 添加对 @{4} 的依赖项,如下所示:[source, xml]

link:{samples_path}/standalone/dsl/http-client/pom.xml[role=include]
  1. 使用 @{5} 注释测试类。在注释中,为存根运行器提供 @{6} 和 @{7} 以下载协作者的存根。[source, groovy]

link:{samples_path}/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[role=include]
  1. (可选)因为您正在离线处理协作者,您还可以提供离线工作开关 (@{8})。

现在,当您运行测试时,您在日志中看到类似于以下内容的输出:

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,并且在端口 8080 上带有 stubs 分类器。

Filing a Pull Request

您迄今为止所做的是一个迭代过程。您可以修改契约、在本地安装它,然后在消费者端上进行操作,直到契约按您希望的那样工作。

一旦您对结果感到满意并且测试通过,您就可以向服务器端发布拉取请求。当前,消费者端的工作已经完成。

The Producer Side (Fraud Detection server)

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

  • 接管拉取请求

  • Write the missing implementation

  • Deploy the application

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

"Fraud\nDetection"->"Fraud\nDetection": take over the\n pull request
"Fraud\nDetection"->"Fraud\nDetection": setup\nSpring Cloud\nContract plugin
"Fraud\nDetection"->"Fraud\nDetection\nBuild": run the build
"Fraud\nDetection\nBuild"->"SCC Plugin": generate tests\nstubs \nand stubs artifact \n(e.g. stubs-jar)
"SCC Plugin"->"Fraud\nDetection\nBuild": tests and stubs generated
"Fraud\nDetection\nBuild"->"Fraud\nDetection\nBuild": run tests
"Fraud\nDetection\nBuild"->"Fraud\nDetection": generated tests failed!
"Fraud\nDetection"->"Fraud\nDetection": setup\nbase classes\nfor contract tests
"Fraud\nDetection"->"Fraud\nDetection\nBuild": run the build
"Fraud\nDetection\nBuild"->"SCC Plugin": generate tests\nstubs \nand stubs artifact \n(e.g. stubs-jar)
"SCC Plugin"->"Fraud\nDetection\nBuild": tests and stubs generated
"Fraud\nDetection\nBuild"->"Fraud\nDetection\nBuild": run tests
"Fraud\nDetection\nBuild"->"Fraud\nDetection": all the tests passed!
"Fraud\nDetection"->"Fraud\nDetection": commit and push changes
"Fraud\nDetection"->"CI": commit pushed!\nTriggers the build
"CI"->"Stub Storage": build successful,\nupload artifacts

Taking over the Pull Request

作为提醒,以下代码列表显示了初始实现:

link:{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[role=include]
link:{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[role=include]
}

然后,您可以运行以下命令:

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

您必须按以下方式添加自动生成测试所需的依赖关系:

link:{samples_path}/standalone/dsl/http-server/pom.xml[role=include]

在 Maven 插件的配置中,您必须按以下方式传递 packageWithBaseClasses 属性:

link:{samples_path}/standalone/dsl/http-server/pom.xml[role=include]

此示例通过设置`packageWithBaseClasses`属性来使用"`convention-based`"命名。这样做意味着最后两个包组合在一起以构成基本测试类的名称。在我们的示例中,合同被放在了`src/test/resources/contracts/fraud`下。由于您没有以`contracts`文件夹为开始的两个包,因此只选择一个包,即`fraud`。添加`Base`后缀并使用大写`fraud`。这样您将得到`FraudBase`测试类名称。

所有生成的测试均扩展该类别。在这里,您可以设置您的 Spring 上下文或一切必需的内容。在此情况下,您应使用 Rest Assured MVC 启动服务器端 FraudDetectionController。以下代码清单显示了 FraudBase 类:

link:{samples_path}/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java[role=include]

现在,如果您运行 ./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,则可以看到 Contract 中的所有 producer() 部分,它们存在于 value(consumer(…​), producer(…​)) 块中,并被注入到测试中。如果您使用 YAML,则同样的内容也适用于 responsematchers 部分。

请注意,在生产者端,您还执行 TDD。期望以测试的形式表达。此测试使用契约中定义的 URL、标题和正文向我们自己的应用程序发送请求。它还预期在响应中精确定义的值。换言之,您有 redgreenrefactor 中的 red 部分。是时候将 red 转换成 green 了。

Write the Missing Implementation

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

link:{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[role=include]
link:{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[role=include]
link:{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[role=include]
}

当您再次运行 ./mvnw clean install 时,测试通过。由于 Spring Cloud Contract Verifier 插件将测试添加到 generated-test-sources 中,因此您实际上可以从 IDE 运行那些测试。

Deploying Your Application

在完成您的工作之后,您可以部署您的更改。为此,您必须先通过运行以下命令合并分支:

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

您的 CI 可能会运行诸如 ./mvnw clean deploy 的命令,该命令会发布应用程序和存根制品。

Consumer Side (Loan Issuance), Final Step

作为贷款发放服务的开发者(欺诈检测服务器的使用者),您需要:

  • 将我们的功能分支合并到 @{9}

  • 切换到在线工作模式

下图 UML 序列图展示了流程的最终状态:

"Loan\nIssuance"->"Loan\nIssuance": merge the\nfeature branch\nto master branch
"Loan\nIssuance"->"Loan\nIssuance": setup SCC Stub Runner\nto fetch stubs\nfrom Stub Storage
"Loan\nIssuance"->"LI\nSCC\nStub Runner": start stubs\nof FD from\nStub Storage
"LI\nSCC\nStub Runner"->"Stub Storage": find stubs of [FD]
"Stub Storage"->"LI\nSCC\nStub Runner": stubs of [FD] found
"LI\nSCC\nStub Runner"->"FD stub": run stubs of [FD]
"FD stub"->"LI\nSCC\nStub Runner": [FD] stub is running
"LI\nSCC\nStub Runner"->"Loan\nIssuance": stubs running and ready for the test
"Loan\nIssuance"->"Loan\nIssuance": run a test
"Loan\nIssuance"->"FD stub": the test\nsends a request\nto the running stub
"FD stub"->"Loan\nIssuance": stub responds successfuly
"Loan\nIssuance"->"Loan\nIssuance": the test passes successfully

Merging a Branch to Master

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

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

Working Online

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

link:{samples_path}/standalone/dsl/http-client/src/test/resources/application-test-repo.yaml[role=include]

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