Step-by-step Guide to Consumer Driven Contracts (CDC) with Contracts on the Producer Side
考虑欺诈检测和贷款发放流程的示例。业务场景是这样,我们想要向人们发放贷款,但不想让他们在我们这里偷窃。我们系统的当前实现向每个人发放贷款。
假设 Loan Issuance
是 Fraud 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
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/pom.xml[]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/build.gradle[]
为简便起见,我们使用以下首字母缩写词:
-
贷款发放 (LI):HTTP 客户端
-
欺诈检测 (FD):HTTP 服务器
-
SCC: Spring Cloud Contract
The Consumer Side (Loan Issuance)
作为贷款发放服务(欺诈检测服务器的使用者)的开发者,你可能会执行如下步骤:
-
通过为你的功能编写测试开始进行 TDD。
-
Write the missing implementation.
-
在本地克隆欺诈检测服务存储库。
-
在欺诈检测服务的存储库中在本地定义合同。
-
添加 Spring Cloud Contract (SCC) 插件。
-
Run the integration tests.
-
File a pull request.
-
Create an initial implementation.
-
接管合并请求。
-
Write the missing implementation.
-
Deploy your application.
-
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
以下清单显示了我们可能用于检查贷款金额是否过大的测试:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[]
假设你已编写了一个新功能测试。如果收到大额贷款申请,系统应拒绝该贷款申请并提供一些说明。
Write the Missing Implementation
在某些时间点,你需要向欺诈检测服务发送一个请求。假设你需要发送一个请求,其中包含客户 ID 和客户希望借贷的金额。你想发送到 /fraudcheck
URL,并使用 PUT
方法。为此,你可能会使用类似于以下的代码:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java[]
为简便起见,欺诈检测服务的端口设置为 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:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/test/resources/contracts/yml/fraud/shouldMarkClientAsFraud.yml[]
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,如下例所示:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/pom.xml[]
然后,添加 Spring Cloud Contract Verifier
Maven 插件,如下例所示:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/pom.xml[]
由于已添加插件,你可以获得 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
) 中执行以下操作:
-
添加 @{3} BOM,如下所示:[source, xml]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-client/pom.xml[]
-
添加对 @{4} 的依赖项,如下所示:[source, xml]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-client/pom.xml[]
-
使用 @{5} 注释测试类。在注释中,为存根运行器提供 @{6} 和 @{7} 以下载协作者的存根。[source, groovy]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[]
-
(可选)因为您正在离线处理协作者,您还可以提供离线工作开关 (@{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
分类器。
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
作为提醒,以下代码列表显示了初始实现:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[]
}
然后,您可以运行以下命令:
$ git checkout -b contract-change-pr master
$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
您必须按以下方式添加自动生成测试所需的依赖关系:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/pom.xml[]
在 Maven 插件的配置中,您必须按以下方式传递 packageWithBaseClasses
属性:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/pom.xml[]
此示例通过设置`packageWithBaseClasses`属性来使用"`convention-based`"命名。这样做意味着最后两个包组合在一起以构成基本测试类的名称。在我们的示例中,合同被放在了`src/test/resources/contracts/fraud`下。由于您没有以`contracts`文件夹为开始的两个包,因此只选择一个包,即`fraud`。添加`Base`后缀并使用大写`fraud`。这样您将得到`FraudBase`测试类名称。
所有生成的测试均扩展该类别。在这里,您可以设置您的 Spring 上下文或一切必需的内容。在此情况下,您应使用 Rest Assured MVC 启动服务器端 FraudDetectionController
。以下代码清单显示了 FraudBase
类:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java[]
现在,如果您运行 ./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,则同样的内容也适用于 response
的 matchers
部分。
请注意,在生产者端,您还执行 TDD。期望以测试的形式表达。此测试使用契约中定义的 URL、标题和正文向我们自己的应用程序发送请求。它还预期在响应中精确定义的值。换言之,您有 red
、green
和 refactor
中的 red
部分。是时候将 red
转换成 green
了。
Write the Missing Implementation
因为您知道预期的输入和预期的输出,因此您可以按以下方式编写缺失的实现:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[]
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[]
}
当您再次运行 ./mvnw clean install
时,测试通过。由于 Spring Cloud Contract Verifier 插件将测试添加到 generated-test-sources
中,因此您实际上可以从 IDE 运行那些测试。
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
。以下代码展示了一个通过更改属性实现相同目标的示例:
Unresolved directive in cdc.adoc - include::{samples_path}/standalone/dsl/http-client/src/test/resources/application-test-repo.yaml[]
就是这样。您已完成教程。