文献作者:Adam Dudczak,MathiasDüsterhöft,Marcin Grzejszczak,Dennis Kieselhorst,JakubKubryński,Karol Lassak,Olga Maciaszek-Sharma,MariuszSmykuła,Dave Syer
1.1.2.RELEASE
Spring Cloud Contract
您始终需要的是将新功能推向分布式系统中的新应用程序或服务的信心。该项目为Spring应用程序中的消费者驱动Contracts和服务架构提供支持,涵盖了一系列用于编写测试的选项,将其作为资产发布,声称生产者和消费者保留合同用于HTTP和消息的交互。
Spring Cloud Contract验证者
介绍
提示
|
Accurest项目最初是由Marcin Grzejszczak和Jakub Kubrynski(codearte.io) |
只是为了简短说明 - Spring Cloud Contract验证程序是一种可以消除驱动合同(CDC)开发基于JVM的应用程序的工具。它与合同定义语言(DSL)一起提供。合同定义用于生成以下资源:
-
在客户端代码(客户端测试)上进行集成测试时,WireMock将使用JSON存根定义。测试代码仍然必须手动编写,测试数据由Spring Cloud Contract验证器生成。
-
消息传递路由,如果你使用一个。我们正在与Spring Integration,Spring Cloud Stream,Spring AMQP和Apache Camel集成。然而,您可以设置自己的集成,如果你想
-
验收测试(在JUnit或Spock中)用于验证API的服务器端实现是否符合合同(服务器测试)。完整测试由Spring Cloud Contract验证器生成。
Spring Cloud Contract验证者将TDD移动到软件体系结构的层次。
为什么?
让我们假设我们有一个由多个微服务组成的系统:

测试问题
如果我们想测试应用程序在左上角,如果它可以与其他服务通信,那么我们可以做两件事之一:
-
部署所有微服务器并执行端到端测试
-
模拟其他微型服务单元/集成测试
两者都有其优点,但也有很多缺点。我们来关注后者。
部署所有微服务器并执行端到端测试
优点:
-
模拟生产
-
测试服务之间的真实沟通
缺点:
-
要测试一个微服务器,我们将不得不部署6个微服务器,几个数据库等。
-
将进行测试的环境将被锁定用于一套测试(即没有人能够在此期间运行测试)。
-
长跑
-
非常迟的反馈
-
非常难调试
模拟其他微型服务单元/集成测试
优点:
-
非常快的反馈
-
无基础架构要求
缺点:
-
服务的实现者创建存根,因此它们可能与现实无关
-
您可以通过测试和生产不合格进行生产
为了解决上述问题,Spring Cloud Contract具有Stub Runner的验证器被创建。他们的主要思想是给您非常快的反馈意见,而无需建立整个微服务的世界。如果您在存根上工作,那么您需要的唯一应用是应用程序直接使用的应用程序。

Spring Cloud Contract验证器确定您使用的存根是由您正在调用的服务创建的。此外,如果您可以使用它们,这意味着它们是针对生产者的一方进行测试的。换句话说 - 你可以信任这些存根。
目的
Spring Cloud Contract验证器与Stub Runner的主要目的是:
-
确保WireMock / Messaging存根(在开发客户端时使用)正在完全实际执行服务器端实现,
-
推广ATDD方法和微服务架构风格,
-
提供一种发布双方立即可见的合同变更的方法,
-
生成服务器端使用的样板测试代码。
重要
|
Spring Cloud Contract验证者的目的不是开始在合同中编写业务功能。我们假设我们有一个欺诈检查的商业用例。如果一个用户因为100个不同的原因而被欺诈,我们假设你会创建2个合同。一个是积极的,一个是负面的欺诈案件。合同测试用于测试应用程序之间的合同,而不是模拟完整行为。 |
怎么样
定义合同
作为消费者,我们需要确定我们想要实现的目标。我们需要制定我们的期望。这就是为什么我们写下面的合同。
我们假设我们要发送包含客户端ID和要从我们借款的金额的请求。我们想通过PUT方法将它发送到/ fraudcheck url。
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
"client.id": $(regex('[0-9]{10}')),
loanAmount: 99999
])
headers { // (5)
contentType('application/json')
}
}
response { // (6)
status 200 // (7)
body([ // (8)
fraudCheckStatus: "FRAUD",
"rejection.reason": "Amount too high"
])
headers { // (9)
contentType('application/json')
}
}
}
/*
From the Consumer perspective, when shooting a request in the integration test:
(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/json`
From the Producer perspective, in the autogenerated producer-side test:
(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/json.*`
*/
客户端
Spring Cloud Contract将生成存根,您可以在客户端测试期间使用。您将拥有一个模拟服务Y并运行的WireMock实例/消息传递路由。您希望为该实例提供适当的存根定义。
在某些时间点,您需要向欺诈检测服务发送请求。
ResponseEntity<FraudServiceResponse> response =
restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
new HttpEntity<>(request, httpHeaders),
FraudServiceResponse.class);
用@AutoConfigureStubRunner
标注你的测试类。在注释中,提供Stub Runner下载协作者存根的组ID和工件ID。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, workOffline = true)
@DirtiesContext
public class LoanApplicationServiceTests {
之后,在测试过程中,Spring Cloud Contract将自动找到Maven存储库中的存根(模拟真正的服务),并将其公开在配置(或随机)端口上。
服务器端
作为开发您的存根的服务Y,您需要确保它实际上类似于您的具体实现。您不能以某种方式存在您的存根行为,并且您的生产应用程序以不同的方式运行。
这就是为什么会生成提供的存根验收测试,这将确保您的应用程序的行为与您在存根中定义的相同。
自动生成测试将如下所示:
@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");
}
逐步向CDC指导
举一个欺诈检测和贷款发行流程的例子。业务情景是这样的,我们想向人们发放贷款,但不希望他们从我们那里偷钱。目前我们的系统实施给大家贷款。
假设Loan Issuance
是Fraud Detection
服务器的客户端。在目前的冲刺中,我们需要开发一个新的功能 - 如果客户想要借到太多的钱,那么我们将他们标记为欺诈。
技术说明 - 欺诈检测将具有工件ID http-server
,贷款发行http-client
,并且都有组ID com.example
。
社会声明 - 客户端和服务器开发团队都需要直接沟通,并在整个过程中讨论变更。CDC是关于沟通的。
提示
|
在这种情况下,合同的所有权在生产者方面。这意味着物理上所有的合同都存在于生产者存储库中 |
技术说明
如果使用SNAPSHOT / 里程碑 / 版本候选版本,请将以下部分添加到您的
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
消费方(贷款发行)
作为贷款发行服务(欺诈检测服务器的消费者)的开发人员:
通过对您的功能进行测试开始做TDD
@Test
public void shouldBeRejectedDueToAbnormalLoanAmount() {
// given:
LoanApplication application = new LoanApplication(new Client("1234567890"),
99999);
// when:
LoanApplicationResult loanApplication = service.loanApplication(application);
// then:
assertThat(loanApplication.getLoanApplicationStatus())
.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
}
我们刚刚写了一个关于我们新功能的测试。如果收到大额的贷款申请,我们应该拒绝有一些描述的贷款申请。
写入缺少的实现
在某些时间点,您需要向欺诈检测服务发送请求。我们假设我们要发送包含客户端ID和要从我们借款的金额的请求。我们想通过PUT
方法将其发送到/fraudcheck
url。
ResponseEntity<FraudServiceResponse> response =
restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
new HttpEntity<>(request, httpHeaders),
FraudServiceResponse.class);
为了简单起见,我们已将欺诈检测服务端口硬编码为8080
,我们的应用程序正在8090
上运行。
如果我们开始写测试,显然会因为端口8080
上没有运行服务而中断。
在本地克隆欺诈检测服务存储库
我们将开始玩服务器端的合同。这就是为什么我们需要先克隆它。
git clone https://your-git-server.com/server-side.git local-http-server-repo
在欺诈检测服务的回购中本地定义合同
作为消费者,我们需要确定我们想要实现的目标。我们需要制定我们的期望。这就是为什么我们写下面的合同。
重要
|
我们将合同放在src/test/resources/contracts/fraud 文件夹下。fraud 文件夹是重要的,因为我们将在生产者的测试基础类名中引用该文件夹。
|
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
"client.id": $(regex('[0-9]{10}')),
loanAmount: 99999
])
headers { // (5)
contentType('application/json')
}
}
response { // (6)
status 200 // (7)
body([ // (8)
fraudCheckStatus: "FRAUD",
"rejection.reason": "Amount too high"
])
headers { // (9)
contentType('application/json')
}
}
}
/*
From the Consumer perspective, when shooting a request in the integration test:
(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/json`
From the Producer perspective, in the autogenerated producer-side test:
(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/json.*`
*/
合同是使用静态类型的Groovy DSL编写的。你可能想知道这些value(client(…), server(…))
部分是什么。通过使用此符号Spring Cloud Contract,您可以定义动态的JSON / URL /等的部分。在标识符或时间戳的情况下,您不想硬编码一个值。你想允许一些不同的值范围。这就是为什么对于消费者端,您可以设置与这些值匹配的正则表达式。您可以通过地图符号或带插值的String来提供身体。
有关更多信息,请参阅文档。我们强烈推荐使用地图符号!
上述合同是双方达成的协议:
-
如果发送了HTTP请求
-
端点
/fraudcheck
上的方法PUT
-
与
client.id
匹配正则表达式[0-9]{10}
和loanAmount
等于99999
的JSON体 -
并且标题
Content-Type
等于application/vnd.fraud.v1+json
-
-
那么HTTP响应将被发送给消费者
-
有状态
200
-
包含JSON主体,
fraudCheckStatus
字段包含值FRAUD
,rejectionReason
字段的值为Amount too high
-
和值为
application/vnd.fraud.v1+json
的Content-Type
标题
-
一旦我们准备好在集成测试中实际检查API,我们需要在本地安装存根
添加Spring Cloud Contract验证程序插件
我们可以添加Maven或Gradle插件 - 在这个例子中,我们将展示如何添加Maven。首先我们需要添加Spring Cloud Contract
BOM。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
接下来,Spring Cloud Contract Verifier
Maven插件
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
</plugin>
自添加插件后,我们从提供的合同中获取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.4.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
)中执行以下操作。
添加Spring Cloud Contract
BOM
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
将依赖关系添加到Spring Cloud Contract Stub Runner
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
用@AutoConfigureStubRunner
标注你的测试类。在注释中,提供Stub Runner下载协作者存根的组ID和工件ID。离线工作开关还可以离线使用协作者(可选步骤)。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, workOffline = true)
@DirtiesContext
public class LoanApplicationServiceTests {
现在如果你运行测试你会看到这样的:
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
,artifact id http-server
,版本为0.0.1-SNAPSHOT
的存根和stubs
分类器的端口8080
。
档案公关
我们到现在为止是一个迭代的过程。我们可以玩合同,安装在本地,在消费者身边工作,直到我们对合同感到满意。
一旦我们对结果感到满意,测试通过将PR发布到服务器端。目前消费者方面的工作已经完成。
生产者方(欺诈检测服务器)
作为欺诈检测服务器(贷款发行服务的服务器)的开发人员:
初步实施
作为提醒,您可以看到初始实现
@RequestMapping(value = "/fraudcheck", method = PUT)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}
接管公关
git checkout -b contract-change-pr master
git pull https://your-git-server.com/server-side-fork.git contract-change-pr
您必须添加自动生成测试所需的依赖关系
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
在Maven插件的配置中,我们传递了packageWithBaseClasses
属性
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
</plugin>
重要
|
我们决定使用“约定”命名,方法是设置packageWithBaseClasses 属性。这意味着最后的两个包将被组合成基本测试类的名称。在我们的情况下,这些合同被放置在src/test/resources/contracts/fraud 下。由于我们从contracts 文件夹开始没有2个包,所以我们只挑选一个是fraud 。我们添加Base 后缀,我们正在大写fraud 。这给了我们FraudBase 测试类名称。
|
这是因为所有生成的测试都会扩展该类。在那里你可以设置你的Spring上下文或任何必要的。在我们的例子中,我们使用Rest Assured MVC来启动服务器端FraudDetectionController
。
package com.example.fraud;
import org.junit.Before;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
public class FraudBase {
@Before
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
new FraudStatsController(stubbedStatsProvider()));
}
private StatsProvider stubbedStatsProvider() {
return fraudType -> {
switch (fraudType) {
case DRUNKS:
return 100;
case ALL:
return 200;
}
return 0;
};
}
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
assert rejectionReason == null;
}
}
现在,如果你运行./mvnw clean install
,你会得到这样的sth:
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");
}
您可以看到value(consumer(…), producer(…))
块中存在的所有producer()
部分合同注入测试。
重要的是在生产者方面,我们也在做TDD。我们有一个测试形式的期望。此测试正在向我们自己的应用程序拍摄一个在合同中定义的URL,标题和主体的请求。它也期待响应中非常精确地定义的值。换句话说,您是red
green
和refactor
的red
部分。将red
转换为green
的时间。
写入缺少的实现
现在,由于我们现在预期的输入和预期的输出是什么,让我们写出缺少的实现。
@RequestMapping(value = "/fraudcheck", method = PUT)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
if (amountGreaterThanThreshold(fraudCheck)) {
return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
}
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}
如果再次执行./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
一样运行,它将发布应用程序和存根工件。
消费方(贷款发行)最后一步
作为贷款发行服务(欺诈检测服务器的消费者)的开发人员:
合并分支到主
git checkout master
git merge --no-ff contract-change-pr
在线工作
现在,您可以禁用Spring Cloud Contract Stub Runner的离线工作,并提供存储库与存根的位置。此时,服务器端的存根将自动从Nexus / Artifactory下载。您可以关闭注释中的workOffline
参数的值。在下面你可以看到通过改变属性来实现相同的例子。
stubrunner:
ids: 'com.example:http-server-dsl:+:stubs:8080'
repositoryRoot: http://repo.spring.io/libs-snapshot
就是这样!
依赖
添加依赖关系的最佳方法是使用正确的starter
依赖关系。
对于stub-runner
使用spring-cloud-starter-stub-runner
,当您使用插件时,只需添加spring-cloud-starter-contract-verifier
。
附加链接
以下可以找到与Spring Cloud Contract验证器和Stub Runner相关的一些资源。请注意,有些可以过时,因为Spring Cloud Contract验证程序项目正在不断发展。
示例
在这里可以找到一些示例。
常问问题
为什么使用Spring Cloud Contract验证器而不是X?
目前Spring Cloud Contract验证器是基于JVM的工具。因此,当您已经为JVM创建软件时,可能是您的第一选择。这个项目有很多非常有趣的功能,但特别是其中一些绝对使Spring Cloud Contract Verifier在消费者驱动合同(CDC)工具的“市场”上脱颖而出。许多最有趣的是:
-
CDC可以通过消息传递
-
清晰易用,静态DSL
-
可以将当前的JSON文件粘贴到合同中,并且仅编辑其元素
-
从定义的合同自动生成测试
-
Stub Runner功能 - 存根在运行时自动从Nexus / Artifactory下载
-
Spring Cloud集成 - 集成测试不需要发现服务
这个值是(consumer(),producer())?
与存根相关的最大挑战之一是可重用性。只有当他们能够被广泛使用时,他们是否会服务于他们的目的。通常使得难点是请求/响应元素的硬编码值。例如日期或ids。想象下面的JSON请求
{
"time" : "2016-10-10 20:10:15",
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
"body" : "foo"
}
和JSON响应
{
"time" : "2016-10-10 21:10:15",
"id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
"body" : "bar"
}
想象一下,通过更改系统中的时钟或提供数据提供者的存根实现,设置time
字段的正确值(让我们假设这个内容是由数据库生成的)所需的痛苦。这也与id
的字段有关。你会创建一个UUID发生器的stubbed实现?没有意义
所以作为一个消费者,你想发送一个匹配任何形式的时间或任何UUID的请求。这样,您的系统将照常工作 - 将生成数据,您不必将任何东西存入。假设在上述JSON的情况下,最重要的部分是body
字段。您可以专注于其他领域,并提供匹配。换句话说,你想要的存根是这样工作的:
{
"time" : "SOMETHING THAT MATCHES TIME",
"id" : "SOMETHING THAT MATCHES UUID",
"body" : "foo"
}
就响应作为消费者而言,您需要具有可操作性的具体价值。所以这样的JSON是有效的
{
"time" : "2016-10-10 21:10:15",
"id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
"body" : "bar"
}
正如您在前几节所看到的,我们从合同中生成测试。所以从生产者的角度看,情况看起来差别很大。我们正在解析提供的合同,在测试中我们想向您的端点发送一个真正的请求。因此,对于请求的生产者来说,我们不能进行任何匹配。我们需要具体的价值观,使制片人的后台能够工作。这样的JSON将是一个有效的:
{
"time" : "2016-10-10 20:10:15",
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
"body" : "foo"
}
另一方面,从合同有效性的角度来看,响应并不一定必须包含time
或id
的具体值。假设您在生产者方面产生这些 - 再次,您必须做很多桩,以确保始终返回相同的值。这就是为什么从生产者那边你可能想要的是以下回应:
{
"time" : "SOMETHING THAT MATCHES TIME",
"id" : "SOMETHING THAT MATCHES UUID",
"body" : "bar"
}
那么您如何才能为消费者提供一次匹配,并为生产者提供具体的价值,反之亦然?在Spring Cloud Contract中,我们允许您提供动态值。这意味着通信双方可能有所不同。你可以传递值:
可以通过value
方法
value(consumer(...), producer(...))
value(stub(...), test(...))
value(client(...), server(...))
或使用$()
方法
$(consumer(...), producer(...))
$(stub(...), test(...))
$(client(...), server(...))
您可以在Contract DSL部分阅读更多信息。
呼叫value()
或$()
告诉您Spring Cloud Contract您将传递动态值。在consumer()
方法内传递消费者端(在生成的存根)中应该使用的值。在producer()
方法中,传递应在生产者端使用的值(在生成的测试中)。
提示
|
如果一方面你已经通过了正则表达式,而你没有通过另一方,那么对方就会自动生成。 |
大多数情况下,您将使用该方法与regex
助手方法。例如consumer(regex('[0-9]{10}'))
。
总而言之,上述情景的合同看起来或多或少是这样的(正则表达式的时间和UUID被简化,很可能是无效的,但是我们希望在这个例子中保持很简单):
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/someUrl'
body([
time : value(consumer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
id: value(consumer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
body: "foo"
])
}
response {
status 200
body([
time : value(producer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
id: value([producer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
body: "bar"
])
}
}
如何做Stubs版本控制?
API版本控制
让我们尝试回答一个真正意义上的版本控制的问题。如果你指的是API版本,那么有不同的方法。
-
使用超媒体,链接,不要通过任何方式版本您的API
-
通过标题/网址传递版本
我不会试图回答一个方法更好的问题。无论适合您的需求,并允许您创造商业价值应被挑选。
假设你做你的API版本。在这种情况下,您应该提供与您支持的许多版本一样多的合同。您可以为每个版本创建一个子文件夹,或将其附加到合同名称 - 无论如何适合您。
JAR版本控制
如果通过版本控制是指包含存根的JAR的版本,那么基本上有两种主要方法。
假设您正在进行连续交付/部署,这意味着您每次通过管道生成新版本的jar时,该jar可以随时进行生产。例如你的jar版本看起来像这样(它建立在20.10.2016在20:15:21):
1.0.0.20161020-201521-RELEASE
在这种情况下,您生成的存根jar将看起来像这样。
1.0.0.20161020-201521-RELEASE-stubs.jar
在这种情况下,您应该在application.yml
或@AutoConfigureStubRunner
中引用存根提供最新版本的存根。您可以通过传递+
号来做到这一点。例
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})
如果版本控制是固定的(例如1.0.4.RELEASE
或2.1.1
),那么您必须设置jar版本的具体值。示例2.1.1。
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"})
开发者或生产者存根
您可以操作分类器,以针对其他服务的存根或部署到生产的存根的当前开发版本来运行测试。一旦达到生产部署,如果您修改构建以使用prod-stubs
分类器来部署存根,那么可以在一个案例中使用dev stub运行测试,另一个则使用prod stub进行测试。
使用开发版存根的测试示例
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})
使用生产版本的存根的测试示例
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:prod-stubs:8080"})
您也可以通过部署管道中的属性传递这些值。
共同回购合同
存储合同以外的另一种方法是将它们保存在一个共同的地方。它可能与消费者无法克隆生产者代码的安全问题相关。另外,如果您在一个地方保留合约,那么作为生产者,您将知道有多少消费者,以及您的本地变更会消费哪些消费者。
回购结构
假设我们有一个坐标为com.example:server
和3个消费者的生产者:client1
,client2
,client3
。然后在具有常见合同的存储库中,您将具有以下设置(您可以在此处查看:
├── com
│ └── example
│ └── server
│ ├── client1
│ │ └── expectation.groovy
│ ├── client2
│ │ └── expectation.groovy
│ ├── client3
│ │ └── expectation.groovy
│ └── pom.xml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
└── assembly
└── contracts.xml
您可以看到下面的斜杠分隔的groupid /
工件id文件夹(com/example/server
),您对3个消费者(client1
,client2
和client3
)有期望。期望是本文档中描述的标准的Groovy DSL合同文件。该存储库必须生成一个将一对一映射到回收内容的JAR文件。
server
文件夹内的pom.xml
示例。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Server Stubs</name>
<description>POM used to install locally stubs for consumer side</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<spring-cloud-contract.version>1.2.0.BUILD-SNAPSHOT</spring-cloud-contract.version>
<spring-cloud-dependencies.version>Edgware.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
<excludeBuildFolders>true</excludeBuildFolders>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- By default it would search under src/test/resources/ -->
<contractsDirectory>${project.basedir}</contractsDirectory>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
你可以看到除了Spring Cloud Contract Maven插件之外没有任何依赖关系。这些垃圾是消费者运行mvn clean install -DskipTests
来本地安装生产者项目的存根的必要条件。
根文件夹中的pom.xml
可以如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Contracts</name>
<description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the producers to generate tests and stubs</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>contracts</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<attach>true</attach>
<descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
<!-- If you want an explicit classifier remove the following line -->
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
它正在使用程序集插件来构建所有合同的JAR。此类设置的示例如下:
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>project</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>/</outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<excludes>
<exclude>**/${project.build.directory}/**</exclude>
<exclude>mvnw</exclude>
<exclude>mvnw.cmd</exclude>
<exclude>.mvn/**</exclude>
<exclude>src/**</exclude>
</excludes>
</fileSet>
</fileSets>
</assembly>
工作流程
工作流程将与Step by step guide to CDC
中的工作流程类似。唯一的区别是生产者不再拥有合同。所以消费者和生产者必须在共同的存储库中处理共同的合同。
消费者
当消费者希望在合同脱机工作,而不是克隆的生产代码,消费者小组克隆的公共储存库,去要求生产者的文件夹(例如com/example/server
)并运行mvn clean install -DskipTests
在本地安装存根从合同转换。
提示
|
您需要在本地安装Maven |
生产者
作为生产者,只需更改Spring Cloud Contract验证器即可提供URL和包含合同的JAR的依赖关系:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
<contractDependency>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
</contractDependency>
</configuration>
</plugin>
使用此设置,将从http://link/to/your/nexus/or/artifactory/or/sth
下载具有groupid com.example.standalone
和artifactid contracts
的JAR。然后将在当地的临时文件夹中解压缩,并将com/example/server
下的合同作为用于生成测试和存根的选择。由于这个惯例,生产者团队将会知道当一些不兼容的更改完成时,哪些消费者团队将被破坏。
其余的流程看起来是一样的。
如何调试生成的测试客户端发送的请求/响应?
生成的测试都以某种形式或时尚的方式依赖于Apache HttpClient进行RestAssured 。HttpClient有一个名为wire logging的工具,它将整个请求和响应记录到HttpClient。Spring Boot具有日志记录通用应用程序属性,用于执行此类操作,只需将其添加到应用程序属性中即可
logging.level.org.apache.http.wire=DEBUG
可以从响应中引用请求吗?
是! 使用版本1.1.0,我们添加了这样一种可能性。在HTTP存根服务器端,我们正在为WireMock提供支持。在其他HTTP服务器存根的情况下,您必须自己实现该方法。
Spring Cloud Contract验证器HTTP
毕业项目
先决条件
为了在WireMock中使用Spring Cloud Contract验证器,您必须使用Gradle或Maven插件。
警告
|
如果您想在项目中使用Spock,则必须单独添加spock-core 和spock-spring 模块。检查Spock文档以获取更多信息
|
添加具有依赖关系的渐变插件
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
}
}
apply plugin: 'groovy'
apply plugin: 'spring-cloud-contract'
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifier_version}"
}
}
dependencies {
testCompile 'org.codehaus.groovy:groovy-all:2.4.6'
// example with adding Spock core and Spock Spring
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
testCompile 'org.spockframework:spock-spring:1.0-groovy-2.4'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
}
毕业和休息保证3.0
默认情况下,Rest Assured 2.x已添加到类路径中。然而,为了让用户有机会使用Rest Assured 3.x,将其添加到plugins类路径就足够了。
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
classpath "io.rest-assured:rest-assured:3.0.2"
classpath "io.rest-assured:spring-mock-mvc:3.0.2"
}
}
depenendencies {
// all dependencies
// you can exclude rest-assured from spring-cloud-contract-verifier
testCompile "io.rest-assured:rest-assured:3.0.2"
testCompile "io.rest-assured:spring-mock-mvc:3.0.2"
}
这样插件将自动看到Rest Assured 3.x存在于类路径上,并相应地修改导入。
Gradle的快照版本
将其他快照存储库添加到您的build.gradle以使用快照版本,每次成功构建后都会自动上传:
buildscript {
repositories {
mavenCentral()
mavenLocal()
maven { url "http://repo.spring.io/snapshot" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/release" }
}
}
添加存根
默认情况下Spring Cloud Contract验证器正在src/test/resources/contracts
目录中查找存根。
包含存根定义的目录被视为一个类名称,每个存根定义被视为单个测试。我们假设它至少包含一个用作测试类名称的目录。如果有多个级别的嵌套目录,除了最后一个级别之外,将被用作包名称。所以具有以下结构
src/test/resources/contracts/myservice/shouldCreateUser.groovy
src/test/resources/contracts/myservice/shouldReturnUser.groovy
Spring Cloud Contract验证程序将使用两种方法创建测试类defaultBasePackage.MyService
-
shouldCreateUser()
-
shouldReturnUser()
运行插件
插件注册自己在check
任务之前被调用。只要您希望它成为构建过程的一部分,您就无所事事。如果您只想生成测试,请调用generateContractTests
任务。
默认设置
默认的Gradle插件设置创建了以下Gradle部分的构建(它是一个伪代码)
contracts {
targetFramework = 'JUNIT'
testMode = 'MockMvc'
generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts")
contractsDslDir = "${project.rootDir}/src/test/resources/contracts"
basePackageForTests = 'org.springframework.cloud.verifier.tests'
stubsOutputDir = project.file("${project.buildDir}/stubs")
// the following properties are used when you want to provide where the JAR with contract lays
contractDependency {
stringNotation = ''
}
contractsPath = ''
contractsWorkOffline = false
contractRepository {
cacheDownloadedContracts(true)
}
}
tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateClientStubs') {
baseName = project.name
classifier = contracts.stubsSuffix
from contractVerifier.stubsOutputDir
}
project.artifacts {
archives task
}
tasks.create(type: Copy, name: 'copyContracts') {
from contracts.contractsDslDir
into contracts.stubsOutputDir
}
verifierStubsJar.dependsOn 'copyContracts'
publishing {
publications {
stubs(MavenPublication) {
artifactId project.name
artifact verifierStubsJar
}
}
}
配置插件
要更改默认配置,只需将contracts
片段添加到您的Gradle配置
contracts {
testMode = 'MockMvc'
baseClassForTests = 'org.mycompany.tests'
generatedTestSourcesDir = project.file('src/generatedContract')
}
配置选项
-
testMode - 定义接受测试的模式。默认情况下,MockMvc基于Spring的MockMvc。也可以将其更改为JaxRsClient或显式为真实的HTTP调用。
-
导入 - 应包含在生成的测试中的导入的数组(例如['org.myorg.Matchers'])。默认为空数组[]
-
staticImports - 应该包含在生成的测试中的静态导入的数组(例如['org.myorg.Matchers。*'])。默认为空数组[]
-
basePackageForTests - 为所有生成的测试指定基础包。默认设置为org.springframework.cloud.verifier.tests
-
baseClassForTests - 所有生成的测试的基类。如果使用Spock测试,默认为
spock.lang.Specification
。 -
packageWithBaseClasses - 而不是为基类提供固定值,您可以提供一个所有基类放置的包。优先于baseClassForTests。
-
baseClassMappings - 明确地将合约包映射到基类的FQN。优先于packageWithBaseClasses和baseClassForTests。
-
ruleClassForTests - 指定应该添加到生成的测试类的规则。
-
ignoredFiles - Ant匹配器允许定义应该跳过哪些处理的存根文件。默认为空数组[]
-
contractsDslDir - 包含使用GroovyDSL编写的合同的目录。默认情况下
$rootDir/src/test/resources/contracts
-
generatedTestSourcesDir - 应该放置从Groovy DSL 生成测试的测试源目录。默认情况下
$buildDir/generated-test-sources/contractVerifier
-
stubsOutputDir - 应该放置从Groovy DSL生成的WireMock存根的目录
-
targetFramework - 要使用的目标测试框架; JUnit作为默认框架,目前支持Spock和JUnit
当您希望提供合同所在JAR的位置时,将使用以下属性
-
contractDependency - 提供
groupid:artifactid:version:classifier
坐标的依赖关系。您可以使用contractDependency
关闭来设置它 -
contractsPath - 如果下载合同订单将默认为
groupid/artifactid
,其中groupid
将被分隔。否则将扫描提供的目录下的合同 -
contractsWorkOffline - 为了不下载依赖关系,每次下载一次,然后离线工作(重用本地Maven repo)
所有测试的单一基类
在默认MockMvc中使用Spring Cloud Contract验证器时,您需要为所有生成的验收测试创建一个基本规范。在这个类中,您需要指向应验证的端点。
abstract class BaseMockMvcSpec extends Specification {
def setup() {
RestAssuredMockMvc.standaloneSetup(new PairIdController())
}
void isProperCorrelationId(Integer correlationId) {
assert correlationId == 123456
}
void isEmpty(String value) {
assert value == null
}
}
在使用Explicit
模式的情况下,您可以像常规集成测试一样使用基类来初始化整个测试的应用程序。在JAXRSCLIENT
模式的情况下,该基类也应该包含protected WebTarget webTarget
字段,现在测试JAX-RS API的唯一选项是启动一个web服务器。
不同的基础类别的合同
如果您的基类在合同之间有所不同,您可以告诉Spring Cloud Contract插件哪个类应该由自动生成测试扩展。你有两个选择:
-
遵循约定,提供
packageWithBaseClasses
-
通过
baseClassMappings
提供显式映射
惯例
约定是这样的,如果你有合同,例如src/test/resources/contract/foo/bar/baz/
,并将packageWithBaseClasses
属性的值提供给com.example.base
,那么我们将假设在com.example.base
下有一个BarBazBase
类包。换句话说,如果它们存在并且形成具有Base
后缀的类,那么我们将使用最后两个包的部分。优先于baseClassForTests。contracts
关闭中的使用示例:
packageWithBaseClasses = 'com.example.base'
制图
您可以手动将合同包的正则表达式映射到匹配合同的基类的完全限定名称。我们来看看下面的例子:
baseClassForTests = "com.example.FooBase"
baseClassMappings {
baseClassMapping('.*/com/.*', 'com.example.ComBase')
baseClassMapping('.*/bar/.*':'com.example.BarBase')
}
我们假设你有合同 - src/test/resources/contract/com/
- src/test/resources/contract/foo/
通过提供baseClassForTests
,我们有一个回退,如果映射没有成功(您也可以提供packageWithBaseClasses
作为备用)。这样,从src/test/resources/contract/com/
合同生成的测试将扩展com.example.ComBase
,而其余的测试将扩展com.example.FooBase
。
调用生成的测试
为了确保提供方对定义的合同进行投诉,您需要调用:
./gradlew generateContractTests test
Spring Cloud Contract验证者在消费者方面
在消费者服务中,您需要以与提供商相同的方式配置Spring Cloud Contract验证程序插件。如果您不想使用Stub Runner,则需要复制存储在src/test/resources/contracts
中的合同,并使用以下命令生成WireMock json存根:
./gradlew generateClientStubs
请注意,必须为存根生成设置stubsOutputDir
选项才能正常工作。
当存在时,json存根可用于消费者自动测试。
@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
class LoanApplicationServiceSpec extends Specification {
@ClassRule
@Shared
WireMockClassRule wireMockRule == new WireMockClassRule()
@Autowired
LoanApplicationService sut
def 'should successfully apply for loan'() {
given:
LoanApplication application =
new LoanApplication(client: new Client(clientPesel: '12345678901'), amount: 123.123)
when:
LoanApplicationResult loanApplication == sut.loanApplication(application)
then:
loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED
loanApplication.rejectionReason == null
}
}
在LoanApplication下面调用FraudDetection服务。此请求由使用Spring Cloud Contract验证器生成的存根配置的WireMock服务器进行处理。
在您的Maven项目中使用
添加maven插件
添加Spring Cloud Contract BOM
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
接下来,Spring Cloud Contract Verifier
Maven插件
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
</plugin>
您可以在Spring Cloud Contract Maven插件文档中阅读更多内容
Maven and Rest Assured 3.0
默认情况下,Rest Assured 2.x已添加到类路径中。然而,为了让用户有机会使用Rest Assured 3.x,将其添加到plugins类路径就足够了。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example</packageWithBaseClasses>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</plugin>
<dependencies>
<!-- all dependencies -->
<!-- you can exclude rest-assured from spring-cloud-contract-verifier -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>3.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
这样插件将自动看到Rest Assured 3.x存在于类路径上,并相应地修改导入。
Maven的快照版本
对于快照/里程碑版本,您必须将以下部分添加到您的pom.xml
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
添加存根
默认情况下Spring Cloud Contract验证器正在寻找src/test/resources/contracts
目录中的存根。包含存根定义的目录被视为一个类名称,每个存根定义被视为单个测试。我们假设它至少包含一个用作测试类名称的目录。如果有多个级别的嵌套目录,除了最后一个级别之外,将被用作包名称。所以具有以下结构
src/test/resources/contracts/myservice/shouldCreateUser.groovy
src/test/resources/contracts/myservice/shouldReturnUser.groovy
Spring Cloud Contract验证者将使用两种方法创建测试类defaultBasePackage.MyService
- shouldCreateUser()
- shouldReturnUser()
运行插件
插件目标generateTests
被分配为在阶段generate-test-sources
中调用。只要您希望它成为构建过程的一部分,您就无所事事。如果您只想生成测试,请调用generateTests
目标。
配置插件
要更改默认配置,只需将configuration
部分添加到插件定义或execution
定义。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>convert</goal>
<goal>generateStubs</goal>
<goal>generateTests</goal>
</goals>
</execution>
</executions>
<configuration>
<basePackageForTests>org.springframework.cloud.verifier.twitter.place</basePackageForTests>
<baseClassForTests>org.springframework.cloud.verifier.twitter.place.BaseMockMvcSpec</baseClassForTests>
</configuration>
</plugin>
重要配置选项
-
testMode - 定义接受测试的模式。默认情况下,
MockMvc
基于Spring的MockMvc。对于真正的HTTP调用,它也可以更改为JaxRsClient
或更改为Explicit
。 -
basePackageForTests - 为所有生成的测试指定基础包。默认设置为
org.springframework.cloud.verifier.tests
。 -
ruleClassForTests - 指定应该添加到生成的测试类的规则。
-
baseClassForTests - 生成测试的基类。如果使用Spock测试,默认为
spock.lang.Specification
。 -
contractsDirectory - 包含使用GroovyDSL编写的合同的目录。默认
/src/test/resources/contracts
。 -
testFramework - 要使用的目标测试框架; JUnit作为默认框架,目前支持Spock和JUnit
-
packageWithBaseClasses - 而不是为基类提供固定值,您可以提供一个所有基类放置的包。约定是这样的,如果你有合同
src/test/resources/contract/foo/bar/baz/
,并将此属性的值提供给com.example.base
,那么我们将假设com.example.base
包下有一个BarBazBase
类。优先于baseClassForTests -
baseClassMappings - 您必须提供
contractPackageRegex
的基类映射列表,该绑定根据合同所在的包进行检查,并且baseClassFQN
映射到匹配合同的基类的完全限定名称。如果您有合同src/test/resources/contract/foo/bar/baz/
并映射属性.*
→com.example.base.BaseClass
,则从这些合同生成的测试类将扩展com.example.base.BaseClass
。优先于packageWithBaseClasses 和baseClassForTests。
如果要从Maven存储库中下载合同定义,可以使用
-
contractDependency - 包含所有打包合同的合同依赖关系
-
contractPath - 通过打包合同在JAR中具体合同的路径。默认为
groupid/artifactid
,其中gropuid
被斜杠分隔。 -
contractWorkOffline - 如果依赖关系应该被下载,或者本地Maven只能被重用
-
contractRepositoryUrl - DEPRECATED PROPERTY - 请使用
contractRepository
关闭 - 具有合同工件的repo的URL(如果不提供)应使用当前的Maven -
contractRepository - 关闭,您可以在其中定义与仓库与合同相关的属性
-
用户名 - 用于连接到repo的用户名
-
密码 - 用于连接到repo的用户名
-
proxyHost - 用于连接到repo的代理主机
-
proxyPort - 用于连接到repo的代理端口
-
cacheDownloadedContracts - 如果要重新使用包含合同定义的下载JAR。我们仅缓存非快照,明确提供的版本(例如,
+
或1.0.0.BUILD-SNAPSHOT
将不会被缓存)。默认情况下,此功能已打开。
-
所有测试的单一基类
在默认MockMvc中使用Spring Cloud Contract验证器时,您需要为所有生成的验收测试创建一个基本规范。在这个类中,您需要指向应验证的端点。
package org.mycompany.tests
import org.mycompany.ExampleSpringController
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
import spock.lang.Specification
class MvcSpec extends Specification {
def setup() {
RestAssuredMockMvc.standaloneSetup(new ExampleSpringController())
}
}
在使用Explicit
模式的情况下,您可以像常规集成测试一样使用基类来初始化整个测试的应用程序。在JAXRSCLIENT
模式的情况下,此基类还应包含protected WebTarget webTarget
字段,现在测试JAX-RS API的唯一选项是启动web服务器。
不同的基础类别的合同
如果您的基类在合同之间有所不同,您可以告诉Spring Cloud Contract插件哪个类应该由自动生成测试扩展。你有两个选择:
-
按照约定,提供
packageWithBaseClasses
-
通过
baseClassMappings
提供显式映射
惯例
约定是,如果您有一个合同,例如src/test/resources/contract/hello/v1/
,并将packageWithBaseClasses
属性的值提供给hello
,那么我们将假设在hello
下有一个HelloV1Base
类包。换句话说,如果它们存在并且形成具有Base
后缀的类,那么我们将使用最后两个包的部分。优先于baseClassForTests。使用示例:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<packageWithBaseClasses>hello</packageWithBaseClasses>
</configuration>
</plugin>
制图
您可以手动将合同包的正则表达式映射到匹配合同的基类的完全限定名称。您必须提供baseClassMapping
contractPackageRegex
到baseClassFQN
映射的列表baseClassMappings
。我们来看看下面的例子:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<baseClassForTests>com.example.FooBase</baseClassForTests>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*com.*</contractPackageRegex>
<baseClassFQN>com.example.TestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
</plugin>
我们假设你有合同 - src/test/resources/contract/com/
- src/test/resources/contract/foo/
通过提供baseClassForTests
,我们有一个后备,如果映射没有成功(你也可以提供packageWithBaseClasses
作为备用)。这样从src/test/resources/contract/com/
合同产生的测试将扩展com.example.ComBase
,而其余的测试将扩展com.example.FooBase
。
调用生成的测试
Spring Cloud Contract Maven插件将验证码生成到目录/generated-test-sources/contractVerifier
中,并将此目录附加到testCompile
目标。
对于Groovy Spock代码使用:
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<testSources>
<testSource>
<directory>${project.basedir}/src/test/groovy</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
<testSource>
<directory>${project.build.directory}/generated-test-sources/contractVerifier</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</testSource>
</testSources>
</configuration>
</plugin>
为确保提供方对定义的合同进行投诉,您需要调用mvn generateTest test
Maven插件常见问题
Maven插件和STS
如果在使用STS时看到以下异常

当您点击标记时,您应该看到这样的sth
plugin:1.1.0.M1:convert:default-convert:process-test-resources) org.apache.maven.plugin.PluginExecutionException: Execution default-convert of goal org.springframework.cloud:spring-
cloud-contract-maven-plugin:1.1.0.M1:convert failed. at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:145) at
org.eclipse.m2e.core.internal.embedder.MavenImpl.execute(MavenImpl.java:331) at org.eclipse.m2e.core.internal.embedder.MavenImpl$11.call(MavenImpl.java:1362) at
...
org.eclipse.core.internal.jobs.Worker.run(Worker.java:55) Caused by: java.lang.NullPointerException at
org.eclipse.m2e.core.internal.builder.plexusbuildapi.EclipseIncrementalBuildContext.hasDelta(EclipseIncrementalBuildContext.java:53) at
org.sonatype.plexus.build.incremental.ThreadBuildContext.hasDelta(ThreadBuildContext.java:59) at
为了解决这个问题,请在pom.xml
中提供以下部分
<build>
<pluginManagement>
<plugins>
<!--This plugin's configuration is used to store Eclipse m2e settings
only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<versionRange>[1.0,)</versionRange>
<goals>
<goal>convert</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute />
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
Spring Cloud Contract验证者在消费者方面
您也可以为消费者使用Spring Cloud Contract验证器!您可以使用插件,以便它只转换合同并生成存根。为了实现这一点,您需要以与提供商相同的方式配置Spring Cloud Contract验证器插件。您需要复制存储在src/test/resources/contracts
中的合同,并使用以下命令生成WireMock json存根:mvn generateStubs
命令。默认情况下,生成的WireMock映射存储在目录target/mappings
中。您的项目应该从此生成的映射创建附加工件与分类器stubs
,以便轻松部署到maven存储库。
示例配置:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${verifier-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>convert</goal>
<goal>generateStubs</goal>
</goals>
</execution>
</executions>
</plugin>
当存在时,json存根可用于消费者自动测试。
@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureStubRunner
public class LoanApplicationServiceTests {
@Autowired
LoanApplicationService service;
@Test
public void shouldSuccessfullyApplyForLoan() {
//given:
LoanApplication application =
new LoanApplication(new Client("12345678901"), 123.123);
//when:
LoanApplicationResult loanApplication = service.loanApplication(application);
// then:
assertThat(loanApplication.loanApplicationStatus).isEqualTo(LoanApplicationStatus.LOAN_APPLIED);
assertThat(loanApplication.rejectionReason).isNull();
}
}
LoanApplication
下方拨打FraudDetection
服务。此请求由使用Spring Cloud Contract验证器生成的存根配置的WireMock服务器进行处理。
方案
可以使用Spring Cloud Contract验证器来处理场景。所有您需要做的是在创建合同时坚持正确的命名约定。公约要求包括后面是下划线的订单编号。
my_contracts_dir\
scenario1\
1_login.groovy
2_showCart.groovy
3_logout.groovy
这样的树将导致Spring Cloud Contract验证器生成名为scenario1
的WireMock场景和三个步骤:
-
登录标记为
Started
,指向: -
showCart标记为
Step1
指向: -
注销标记为
Step2
,这将关闭场景。
有关WireMock场景的更多详细信息,请参见http://wiremock.org/stateful-behaviour.html
Spring Cloud Contract验证器还将生成具有保证执行顺序的测试。
存根和传递依赖
我们创建的Maven和Gradle插件是为您添加创建存根jar的任务。可能有问题的是,当重用存根时,您可以错误地导入所有这些存根依赖关系!即使你有几个不同的罐子,建造一个Maven的工件,他们都有一个pom:
├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar
├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar.sha1
├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar
├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar.sha1
├── github-webhook-0.0.1.BUILD-SNAPSHOT.jar
├── github-webhook-0.0.1.BUILD-SNAPSHOT.pom
├── github-webhook-0.0.1.BUILD-SNAPSHOT-stubs.jar
├── ...
└── ...
使用这些依赖关系有三种可能性,以便不会对传递依赖性产生任何问题。
将所有应用程序依赖项标记为可选
如果在github-webhook
应用程序中,我们将所有的依赖项标记为可选的,当将github-webhook
存根包含在另一个应用程序中(或者当依赖关系由Stub Runner下载)时),因为所有的依赖关系是可选的,它们不会被下载。
为存根创建一个单独的artifactid
如果你创建一个单独的artifactid,那么你可以设置任何你想要的方式。例如通过没有依赖关系。
排除消费者方面的依赖关系
作为消费者,如果将stub依赖关系添加到类路径中,则可以显式排除不需要的依赖关系。
Spring Cloud Contract验证程序消息传递
Spring Cloud Contract验证器允许您验证使用消息传递作为通信手段的应用程序。我们所有的集成都在使用Spring,但您也可以自己创建并使用它。
集成
您可以使用四种集成配置之一:
-
阿帕奇骆驼
-
Spring Integration
-
Spring Cloud Stream
-
Spring AMQP
由于我们使用Spring Boot,因此如果您已将上述库之一添加到类路径中,那么将自动设置所有消息配置。
重要
|
记住将@AutoConfigureMessageVerifier 放在生成的测试的基类上。否则Spring Cloud Contract验证器的消息传递部分将无法正常工作。
|
重要
|
如果要使用Spring Cloud Stream记住添加org.springframework.cloud:spring-cloud-stream-test-support 依赖项。
|
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
手动集成测试
测试使用的主界面是org.springframework.cloud.contract.verifier.messaging.MessageVerifier
。它定义了如何发送和接收消息。您可以创建自己的实现来实现相同的目标。
在测试中,您可以注册ContractVerifierMessageExchange
发送和接收遵循合同的消息。然后将@AutoConfigureMessageVerifier
添加到您的测试中,例如
@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {
@Autowired
private MessageVerifier verifier;
...
}
注意
|
如果您的测试也需要存根,则@AutoConfigureStubRunner 包括消息传递配置,因此您只需要一个注释。
|
发行人端测试一代
在您的DSL中拥有input
或outputMessage
部分将导致在发布商方面创建测试。默认情况下,将创建JUnit测试,但是也可以创建Spock测试。
我们应该考虑三个主要场景:
-
情况1:没有输入消息产生输出消息。输出消息由应用程序内部的组件触发(例如调度程序)
-
情况2:输入消息触发输出消息
-
方案3:输入消息被消耗,没有输出消息
重要
|
传递给messageFrom 或sentTo 的目标对于不同的消息传递实现可以有不同的含义。对于流和集成,它首先被解析为destination 的频道,然后如果没有这样的destination ,它被解析为频道名称。对于骆驼来说,这是一个特定的组件(例如jms )。
|
骆驼示例:
情景1(无输入讯息)
对于给定的合同:
def contractDsl = Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
将创建以下JUnit测试:
'''
// when:
bookReturnedTriggered();
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
assertThat(response).isNotNull();
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
assertThat(response.getHeader("contentType")).isNotNull();
assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
'''
并且将创建以下Spock测试:
'''
when:
bookReturnedTriggered()
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('activemq:output')
assert response != null
response.getHeader('BOOK-NAME')?.toString() == 'foo'
response.getHeader('contentType')?.toString() == 'application/json'
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
'''
情景2(输入触发输出)
对于给定的合同:
def contractDsl = Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
将创建以下JUnit测试:
'''
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header"));
// when:
contractVerifierMessaging.send(inputMessage, "jms:input");
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
assertThat(response).isNotNull();
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
'''
并且将创建以下Spock测试:
"""\
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
'''{"bookName":"foo"}''',
['sample': 'header']
)
when:
contractVerifierMessaging.send(inputMessage, 'jms:input')
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('jms:output')
assert response !- null
response.getHeader('BOOK-NAME')?.toString() == 'foo'
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
"""
情景3(无输出讯息)
对于给定的合同:
def contractDsl = Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
将创建以下JUnit测试:
'''
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header"));
// when:
contractVerifierMessaging.send(inputMessage, "jms:delete");
// then:
bookWasDeleted();
'''
并且将创建以下Spock测试:
'''
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
\'\'\'{"bookName":"foo"}\'\'\',
['sample': 'header']
)
when:
contractVerifierMessaging.send(inputMessage, 'jms:delete')
then:
noExceptionThrown()
bookWasDeleted()
'''
消费者存根侧代
与HTTP部分不同 - 在消息传递中,我们需要使用存根发布JAR中的Groovy DSL。然后在消费者端进行解析,创建适当的stubbed路由。
有关更多信息,请参阅Stub Runner消息部分。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Spring Cloud Contract Stub Runner
使用Spring Cloud Contract验证程序时可能遇到的一个问题是将生成的WireMock JSON存根从服务器端传递到客户端(或各种客户端)。在消息传递的客户端生成方面也是如此。
复制JSON文件/手动设置客户端进行消息传递是不成问题的。
这就是为什么我们会介绍可以为您自动下载和运行存根的Spring Cloud Contract Stub Runner。
快照版本
将其他快照存储库添加到您的build.gradle以使用快照版本,每次成功构建后都会自动上传:
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
将存根发布为JAR
最简单的方法是集中保留存根的方式。例如,您可以将它们作为JAR存储在Maven存储库中。
提示
|
对于Maven和Gradle来说,安装程序都是开箱即用的。但是如果你想要的话可以自定义它。 |
<!-- First disable the default jar setup in the properties section-->
<!-- we don't want the verifier to do a jar for us -->
<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
<!-- Next add the assembly plugin to your build -->
<!-- we want the assembly plugin to generate the JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>stub</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<inherited>false</inherited>
<configuration>
<attach>true</attach>
<descriptor>${basedir}/src/assembly/stub.xml</descriptor>
</configuration>
</execution>
</executions>
</plugin>
<!-- Finally setup your assembly. Below you can find the contents of src/main/assembly/stub.xml -->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>stubs</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/java</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>**com/example/model/*.*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}/classes</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>**com/example/model/*.*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}/snippets/stubs</directory>
<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory>
<includes>
<include>**/*</include>
</includes>
</fileSet>
<fileSet>
<directory>${basedir}/src/test/resources/contracts</directory>
<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory>
<includes>
<include>**/*.groovy</include>
</includes>
</fileSet>
</fileSets>
</assembly>
Stub Runner核心
为服务合作者运行存根。处理作为服务合同的存根允许使用stub-runner作为 Consumer Driven Contracts的实现。
Stub Runner允许您自动下载提供的依赖关系的存根(或从类路径中选择),启动WireMock服务器并为其提供适当的存根定义。对于消息传递,定义了特殊的存根路由。
检索存根
您可以选择采集存根的以下选项
-
基于Aether的解决方案,可从Artifactory / Nexus下载JAR
-
Classpath扫描解决方案,通过模式搜索classpath以检索存根
-
编写自己的实现
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder
进行完全定制
后一个例子在Custom Stub Runner部分中描述。
存根下载
如果您提供stubrunner.repositoryRoot
或stubrunner.workOffline
标志将设置为true
,则Stub Runner将连接到给定的服务器并下载所需的jar。然后,它将打开JAR到一个临时文件夹,并在进一步的合同处理中引用这些文件。
例:
@AutoConfigureStubRunner(repositoryRoot="http://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095")
类路径扫描
如果不提供stubrunner.repositoryRoot
和stubrunner.workOffline
标志将被设置为false
(这是默认值),则classpath将被扫描。我们来看下面的例子:
@AutoConfigureStubRunner(ids = {
"com.example:beer-api-producer:+:stubs:8095",
"com.example.foo:bar:1.0.0:superstubs:8096"
})
如果您已经将依赖项添加到类路径中
<dependency>
<groupId>com.example</groupId>
<artifactId>beer-api-producer-restdocs</artifactId>
<classifier>stubs</classifier>
<version>0.0.1-SNAPSHOT</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.example.foo</groupId>
<artifactId>bar</artifactId>
<classifier>superstubs</classifier>
<version>1.0.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
然后您的类路径上的以下位置将被扫描。对于com.example:beer-api-producer-restdocs
-
/META-INF/com.example/beer-api-producer-restdocs/ * / *。
-
/contracts/com.example/beer-api-producer-restdocs/ * / *。
-
/mappings/com.example/beer-api-producer-restdocs/ * / *。
和com.example.foo:bar
-
/META-INF/com.example.foo/bar/ * / *。
-
/contracts/com.example.foo/bar/ * / *。
-
/mappings/com.example.foo/bar/ * / *。
提示
|
您可以看到,在打包生成器存根时,必须显式提供组和工件ID。 |
生产者将按照以下方式设置合同:
└── src
└── test
└── resources
└── contracts
└── com.example
└── beer-api-producer-restdocs
└── nested
└── contract3.groovy
实现适当的短截线包装。
或者使用Maven assembly
插件或
Gradle Jar任务,您必须在存根jar中创建以下结构。
└── META-INF
└── com.example
└── beer-api-producer-restdocs
└── 2.0.0
├── contracts
│ └── nested
│ └── contract2.groovy
└── mappings
└── mapping.json
通过维护此结构,类路径被扫描,您可以从消息传递/ HTTP存根获取利益,而无需下载工件。
运行存根
限制
重要
|
StubRunner可能会在测试之间关闭端口时出现问题。您可能会遇到您遇到端口冲突的情况。只要您在测试中使用相同的上下文,一切正常。但是当上下文不同(例如不同的存根或不同的配置文件)时,您必须使用@DirtiesContext 关闭存根服务器,否则在每次测试的不同端口上运行。
|
运行使用主应用程序
您可以将以下选项设置为主类:
-c, --classifier Suffix for the jar containing stubs (e.
g. 'stubs' if the stub jar would
have a 'stubs' classifier for stubs:
foobar-stubs ). Defaults to 'stubs'
(default: stubs)
--maxPort, --maxp <Integer> Maximum port value to be assigned to
the WireMock instance. Defaults to
15000 (default: 15000)
--minPort, --minp <Integer> Minimum port value to be assigned to
the WireMock instance. Defaults to
10000 (default: 10000)
-p, --password Password to user when connecting to
repository
--phost, --proxyHost Proxy host to use for repository
requests
--pport, --proxyPort [Integer] Proxy port to use for repository
requests
-r, --root Location of a Jar containing server
where you keep your stubs (e.g. http:
//nexus.
net/content/repositories/repository)
-s, --stubs Comma separated list of Ivy
representation of jars with stubs.
Eg. groupid:artifactid1,groupid2:
artifactid2:classifier
-u, --username Username to user when connecting to
repository
--wo, --workOffline Switch to work offline. Defaults to
'false'
HTTP存根
存根在JSON文档中定义,其语法在WireMock文档中定义
例:
{
"request": {
"method": "GET",
"url": "/ping"
},
"response": {
"status": 200,
"body": "pong",
"headers": {
"Content-Type": "text/plain"
}
}
}
查看注册的映射
每个stubbed协作者公开在__/admin/
端点下定义的映射列表。
消息存根
根据提供的Stub Runner依赖关系和DSL,消息路由将自动设置。
Stub Runner JUnit规则
Stub Runner附带一个JUnit规则,感谢您可以轻松地下载并运行给定组和工件ID的存根:
@ClassRule public static StubRunnerRule rule = new StubRunnerRule()
.repoRoot(repoRoot())
.downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
.downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer");
该规则执行后Stub Runner连接到您的Maven存储库,给定的依赖关系列表尝试:
-
下载它们
-
在本地缓存
-
将它们解压缩到临时文件夹
-
从提供的端口/提供的端口范围的随机端口上为每个Maven依赖关系启动WireMock服务器
-
为WireMock服务器提供所有具有有效WireMock定义的JSON文件
-
也可以发送消息(记得通过一个
MessageVerifier
接口的实现)
Stub Runner使用Eclipse Aether机制下载Maven依赖项。查看他们的文档了解更多信息。
由于StubRunnerRule
实现StubFinder
,它允许您找到已启动的存根:
package org.springframework.cloud.contract.stubrunner;
import java.net.URL;
import java.util.Collection;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
public interface StubFinder extends StubTrigger {
/**
* For the given groupId and artifactId tries to find the matching
* URL of the running stub.
*
* @param groupId - might be null. In that case a search only via artifactId takes place
* @return URL of a running stub or throws exception if not found
*/
URL findStubUrl(String groupId, String artifactId) throws StubNotFoundException;
/**
* For the given Ivy notation {@code [groupId]:artifactId:[version]:[classifier]} tries to
* find the matching URL of the running stub. You can also pass only {@code artifactId}.
*
* @param ivyNotation - Ivy representation of the Maven artifact
* @return URL of a running stub or throws exception if not found
*/
URL findStubUrl(String ivyNotation) throws StubNotFoundException;
/**
* Returns all running stubs
*/
RunningStubs findAllRunningStubs();
/**
* Returns the list of Contracts
*/
Map<StubConfiguration, Collection<Contract>> getContracts();
}
Spock测试中使用示例:
@ClassRule @Shared StubRunnerRule rule = new StubRunnerRule()
.repoRoot(StubRunnerRuleSpec.getResource("/m2repo/repository").toURI().toString())
.downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
.downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
def 'should start WireMock servers'() {
expect: 'WireMocks are running'
rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
rule.findStubUrl('loanIssuance') != null
rule.findStubUrl('loanIssuance') == rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
and:
rule.findAllRunningStubs().isPresent('loanIssuance')
rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
and: 'Stubs were registered'
"${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
}
JUnit测试中的使用示例:
@Test
public void should_start_wiremock_servers() throws Exception {
// expect: 'WireMocks are running'
then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")).isNotNull();
then(rule.findStubUrl("loanIssuance")).isNotNull();
then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance"));
then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isNotNull();
// and:
then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue();
then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs", "fraudDetectionServer")).isTrue();
then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isTrue();
// and: 'Stubs were registered'
then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")).isEqualTo("loanIssuance");
then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")).isEqualTo("fraudDetectionServer");
}
有关如何应用Stub Runner的全局配置的更多信息,请查看JUnit和Spring的公共属性。请查看JUnit的通用属性和{19 /}。
重要
|
要使用JUnit规则和消息传递,您必须提供对规则构建器(例如rule.messageVerifier(new MyMessageVerifier()) )的MessageVerifier 接口的实现。如果您不这样做,那么每当您尝试发送消息时,将抛出异常。
|
Maven设置
存根下载器为不同的本地存储库文件夹授予Maven设置。目前没有考虑存储库和配置文件的身份验证详细信息,因此您需要使用上述属性进行指定。
提供固定端口
您还可以在固定端口上运行您的存根。你可以通过两种不同的方法来实现。一个是在属性中传递它,另一个是通过JUnit规则的流畅API。
流畅的API
使用StubRunnerRule
时,您可以添加一个存根下载,然后通过上次下载的存根的端口。
@ClassRule public static StubRunnerRule rule = new StubRunnerRule()
.repoRoot(repoRoot())
.downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
.withPort(12345)
.downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:12346");
您可以看到,对于此示例,以下测试是有效的:
then(rule.findStubUrl("loanIssuance")).isEqualTo(URI.create("http://localhost:12345").toURL());
then(rule.findStubUrl("fraudDetectionServer")).isEqualTo(URI.create("http://localhost:12346").toURL());
Stub Runner与Spring
设置Stub Runner项目的Spring配置。
通过在配置文件中提供存根列表,Stub Runner自动在WireMock中下载并注册所选的存根。
如果你想查找你的stubbed依赖的URL,你可以自动连接StubFinder
接口并使用它的方法如下:
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(properties = [" stubrunner.cloud.enabled=false",
"stubrunner.camel.enabled=false",
'foo=${stubrunner.runningstubs.fraudDetectionServer.port}'])
@AutoConfigureStubRunner
@DirtiesContext
@ActiveProfiles("test")
class StubRunnerConfigurationSpec extends Specification {
@Autowired StubFinder stubFinder
@Autowired Environment environment
@Value('${foo}') Integer foo
@BeforeClass
@AfterClass
void setupProps() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
def 'should start WireMock servers'() {
expect: 'WireMocks are running'
stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
stubFinder.findStubUrl('loanIssuance') != null
stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance')
stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs')
stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
and:
stubFinder.findAllRunningStubs().isPresent('loanIssuance')
stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
and: 'Stubs were registered'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
}
def 'should throw an exception when stub is not found'() {
when:
stubFinder.findStubUrl('nonExistingService')
then:
thrown(StubNotFoundException)
when:
stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId')
then:
thrown(StubNotFoundException)
}
def 'should register started servers as environment variables'() {
expect:
environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null
stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer)
and:
environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer)
}
def 'should be able to interpolate a running stub in the passed test property'() {
given:
int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
expect:
fraudPort > 0
environment.getProperty("foo", Integer) == fraudPort
foo == fraudPort
}
@Configuration
@EnableAutoConfiguration
static class Config {}
}
对于以下配置文件:
stubrunner:
repositoryRoot: classpath:m2repo/repository/
ids:
- org.springframework.cloud.contract.verifier.stubs:loanIssuance
- org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer
- org.springframework.cloud.contract.verifier.stubs:bootService
cloud:
enabled: false
camel:
enabled: false
spring.cloud:
consul.enabled: false
service-registry.enabled: false
您也可以使用@AutoConfigureStubRunner
内的属性代替使用属性。下面您可以通过设置注释的值来找到实现相同结果的示例。
@AutoConfigureStubRunner(
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
repositoryRoot = "classpath:m2repo/repository/")
Stub Runner Spring为每个注册的WireMock服务器以以下方式注册环境变量。Stub Runner ids com.example:foo
,com.example:bar
的示例。
-
stubrunner.runningstubs.foo.port
-
stubrunner.runningstubs.bar.port
你可以在你的代码中引用它。
Stub Runner Spring Cloud
Stub Runner可以与Spring Cloud集成。
对于现实生活中的例子,你可以检查
Stubbing服务发现
Stub Runner Spring Cloud
的最重要的特征就是它的存在
-
DiscoveryClient
-
Ribbon
ServerList
这意味着无论您是否使用Zookeeper,Consul,Eureka或其他任何事情,您都不需要在测试中。我们正在启动您的依赖关系的WireMock实例,只要您直接使用Feign
,负载平衡RestTemplate
或DiscoveryClient
,就会告诉您的应用程序来调用这些stubbed服务器,而不是调用真正的服务发现工具。
例如这个测试将通过
def 'should make service discovery work'() {
expect: 'WireMocks are running'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}
对于以下配置文件
spring.cloud:
zookeeper.enabled: false
consul.enabled: false
eureka.client.enabled: false
stubrunner:
camel.enabled: false
idsToServiceIds:
ivyNotation: someValueInsideYourCode
fraudDetectionServer: someNameThatShouldMapFraudDetectionServer
测试配置文件和服务发现
在集成测试中,您通常不想既不调用发现服务(例如Eureka)或调用服务器。这就是为什么你创建一个额外的测试配置,你要禁用这些功能。
由于spring-cloud-commons
实现这一点的某些限制,您可以通过下面的静态块(Eureka的示例)禁用这些属性)
//Hack to work around https://github.com/spring-cloud/spring-cloud-commons/issues/156
static {
System.setProperty("eureka.client.enabled", "false");
System.setProperty("spring.cloud.config.failFast", "false");
}
附加配置
您可以使用stubrunner.idsToServiceIds:
地图将存根的artifactId与应用程序的名称进行匹配。您可以通过以下方法来禁用Stub Runner Ribbon支持:stubrunner.cloud.ribbon.enabled
等于false
您可以通过提供:stubrunner.cloud.enabled
等于false
来禁用Stub Runner支持
提示
|
默认情况下,所有服务发现都将被删除。这意味着不管事实如果你有一个现有的DiscoveryClient ,它的结果将被忽略。但是,如果要重用它,只需将stubrunner.cloud.delegate.enabled 设置为true ,然后将您现有的DiscoveryClient 结果与被删除的结果合并。
|
Stub Runner启动应用程序
Spring Cloud Contract验证器Stub Runner Boot是一个Spring Boot应用程序,它暴露了REST端点来触发邮件标签并访问启动的WireMock服务器。
其中一个用例是在部署的应用程序上运行一些烟雾(端到端)测试。您可以在Too Much Coding博客的“Microservice部署”文章中阅读更多信息。
如何使用它?
只需添加
compile "org.springframework.cloud:spring-cloud-starter-stub-runner"
用@EnableStubRunnerServer
注释一个类,构建一个胖子,你准备好了!
对于属性,请检查Stub Runner Spring部分。
端点
HTTP
-
GET
/stubs
- 返回ivy:integer
表示法中所有正在运行的存根的列表 -
GET
/stubs/{ivy}
- 返回给定的ivy
符号的端口(当调用端点ivy
也可以是artifactId
)
消息
消息传递
-
GET
/triggers
- 返回ivy : [ label1, label2 …]
表示法中所有正在运行的标签的列表 -
POST
/triggers/{label}
- 执行label
的触发器 -
POST
/triggers/{ivy}/{label}
- 对于给定的ivy
符号(当调用端点ivy
也可以是artifactId
)时,使用label
执行触发)
例
@ContextConfiguration(classes = StubRunnerBoot, loader = SpringBootContextLoader)
@SpringBootTest(properties = "spring.cloud.zookeeper.enabled=false")
@ActiveProfiles("test")
class StubRunnerBootSpec extends Specification {
@Autowired StubRunning stubRunning
def setup() {
RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
new TriggerController(stubRunning))
}
def 'should return a list of running stub servers in "full ivy:port" notation'() {
when:
String response = RestAssuredMockMvc.get('/stubs').body.asString()
then:
def root = new JsonSlurper().parseText(response)
root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
}
def 'should return a port on which a [#stubId] stub is running'() {
when:
def response = RestAssuredMockMvc.get("/stubs/${stubId}")
then:
response.statusCode == 200
response.body.as(Integer) > 0
where:
stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
'org.springframework.cloud.contract.verifier.stubs:bootService:+',
'org.springframework.cloud.contract.verifier.stubs:bootService',
'bootService']
}
def 'should return 404 when missing stub was called'() {
when:
def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
then:
response.statusCode == 404
}
def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
when:
String response = RestAssuredMockMvc.get('/triggers').body.asString()
then:
def root = new JsonSlurper().parseText(response)
root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book","return_book_1","return_book_2"])
}
def 'should trigger a messaging label'() {
given:
StubRunning stubRunning = Mock()
RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
when:
def response = RestAssuredMockMvc.post("/triggers/delete_book")
then:
response.statusCode == 200
and:
1 * stubRunning.trigger('delete_book')
}
def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
given:
StubRunning stubRunning = Mock()
RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
when:
def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
then:
response.statusCode == 200
and:
1 * stubRunning.trigger(stubId, 'delete_book')
where:
stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
}
def 'should throw exception when trigger is missing'() {
when:
RestAssuredMockMvc.post("/triggers/missing_label")
then:
Exception e = thrown(Exception)
e.message.contains("Exception occurred while trying to return [missing_label] label.")
e.message.contains("Available labels are")
e.message.contains("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
e.message.contains("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
}
}
Stub Runner启动服务发现
使用Stub Runner Boot的可能性之一就是将其用作“烟雾测试”的存根。这是什么意思?假设您不想将50个微服务部署到测试环境中,以检查您的应用程序是否正常工作。您在构建过程中已经执行了一系列测试,但您也希望确保应用程序的打包正常。您可以做的是将应用程序部署到环境中,启动并运行一些测试,以确定它是否正常工作。我们可以将这些测试称为烟雾测试,因为他们的想法只是检查一些测试场景。
这种方法的问题是,如果您正在执行微服务,则很可能您正在使用服务发现工具。Stub Runner引导允许您通过启动所需的存根并将其注册到服务发现工具中来解决此问题。我们来看看这样一个使用Eureka设置的例子。假设Eureka已经在运行。
@SpringBootApplication
@EnableStubRunnerServer
@EnableEurekaClient
@AutoConfigureStubRunner
public class StubRunnerBootEurekaExample {
public static void main(String[] args) {
SpringApplication.run(StubRunnerBootEurekaExample.class, args);
}
}
如您所见,我们希望启动一个Stub Runner引导服务器@EnableStubRunnerServer
,启用Eureka客户端@EnableEurekaClient
,并且我们想要使存根转移功能打开@AutoConfigureStubRunner
。
现在我们假设我们要启动这个应用程序,以便自动注册存根。我们可以通过运行应用程序java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar
来执行此操作,其中${SYSTEM_PROPS}
将包含以下属性列表
-Dstubrunner.repositoryRoot=http://repo.spring.io/snapshots (1)
-Dstubrunner.cloud.stubbed.discovery.enabled=false (2)
-Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework.cloud.contract.verifier.stubs:bootService (3)
-Dstubrunner.idsToServiceIds.fraudDetectionServer=someNameThatShouldMapFraudDetectionServer (4)
(1) - we tell Stub Runner where all the stubs reside
(2) - we don't want the default behaviour where the discovery service is stubbed. That's why the stub registration will be picked
(3) - we provide a list of stubs to download
(4) - we provide a list of artifactId to serviceId mapping
这样您的部署应用程序可以通过服务发现将请求发送到启动的WireMock服务器。最可能的点1-3可以在application.yml
中默认设置,因为它们不太可能改变。这样,只要您启动Stub Runner引导,您只能提供要下载的存根列表。
每消费者存根
有一些情况下,同一终端的2个消费者希望拥有2个不同的响应。
提示
|
这种方法还允许您立即知道哪个消费者正在使用您的API的哪个部分。您可以删除您的API生成的一部分响应,您可以看到哪些自动生成测试失败。如果没有失败,那么你可以安全地删除那个部分的响应,因为没有人在使用它。 |
我们来看看下面的例子,为为producer
生产者定义的合同。有两个消费者:foo-consumer
和bar-consumer
。
消费者foo-service
request {
url '/foo'
method GET()
}
response {
status 200
body(
foo: "foo"
}
}
消费者bar-service
request {
url '/foo'
method GET()
}
response {
status 200
body(
bar: "bar"
}
}
您不能为同一请求产生2个不同的响应。这就是为什么您可以正确地打包合同,然后从stubsPerConsumer
功能中获利。
在生产者方面,消费者可以有一个文件夹,其中包含只与他们有关的合同。通过将stubrunner.stubs-per-consumer
标志设置为true
,我们不再注册所有存根,只能注册与消费者应用程序名称相对应的存根。换句话说,我们将扫描每个存根的路径,如果它包含的子文件夹中的消息者的名称只在路径中,那么它将被注册。
在foo
生产者方面,合约看起来像这样
.
└── contracts
├── bar-consumer
│ ├── bookReturnedForBar.groovy
│ └── shouldCallBar.groovy
└── foo-consumer
├── bookReturnedForFoo.groovy
└── shouldCallFoo.groovy
作为bar-consumer
消费者,您可以将spring.application.name
或stubrunner.consumer-name
设置为bar-consumer
或设置以下测试:
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(properties = ["spring.application.name=bar-consumer"])
@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
repositoryRoot = "classpath:m2repo/repository/",
stubsPerConsumer = true)
@DirtiesContext
class StubRunnerStubsPerConsumerSpec extends Specification {
...
}
然后只允许引用在其名称下包含bar-consumer
的路径下注册的存根(即来自src/test/resources/contracts/bar-consumer/some/contracts/…
文件夹的那些)。
或者明确设置消费者名称
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest
@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
repositoryRoot = "classpath:m2repo/repository/",
consumerName = "foo-consumer",
stubsPerConsumer = true)
@DirtiesContext
class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
...
}
然后只允许引用在其名称下包含foo-consumer
的路径下注册的存根(即来自src/test/resources/contracts/foo-consumer/some/contracts/…
文件夹的那些)。
有关此更改背后原因的更多信息,可以查看问题224。
共同
JUnit和Spring的常用属性
可以使用系统属性或配置属性(对于Spring)设置重复的某些属性。以下是他们的名称及其默认值:
物业名称 | 默认值 | 描述 |
---|---|---|
stubrunner.minPort |
10000 |
具有存根的起始WireMock端口的最小值 |
stubrunner.maxPort |
15000 |
具有存根的起始WireMock端口的最小值 |
stubrunner.repositoryRoot |
Maven repo网址 如果空白,那么将调用本地的maven repo |
|
stubrunner.classifier |
存根 |
stub工件的默认分类器 |
stubrunner.workOffline |
假 |
如果为true,则不会联系任何远程存储库以下载存根 |
stubrunner.ids |
数组的常春藤符号存根下载 |
|
stubrunner.username |
可选的用户名访问使用存根存储JAR的工具 |
|
stubrunner.password |
访问使用存根存储JAR的工具的可选密码 |
|
stubrunner.stubsPerConsumer |
假 |
如果要为每个使用者使用不同的存根,而不是为每个消费者注册所有存根,请设置为 |
stubrunner.consumerName |
如果要使用每个使用者的存根,并想要覆盖消费者名称,只需更改此值 |
存根运动员短桩ids
您可以通过stubrunner.ids
系统属性提供存根下载。他们遵循以下模式:
groupId:artifactId:version:classifier:port
version
,classifier
和port
是可选的。
-
如果您不提供
port
,则会选择一个随机的 -
如果您不提供
classifier
,那么默认值将被采用。(注意,你可以传递这样一个空的分类器groupId:artifactId:version:
) -
如果您不提供
version
,则将通过+
,最新的将被下载
其中port
表示WireMock服务器的端口。
重要
|
从版本1.0.4开始,您可以提供一系列您希望Stub Runner考虑的版本。您可以在这里阅读有关Aether版本控制范围的更多信息。 |
取自Aether文件:
该方案接受任何形式的版本,将版本解释为数字和字母段的序列。字符' - ','_'和'。' 以及从数字到字母的转换,反之亦然分隔版本段。分隔符被视为等同物。
数字段在数学上进行比较,字母段被字典和区分大小写比较。但是,以下限定字符串被特别识别和处理:“alpha”=“a”<“beta”=“b”<“milestone”=“m”<“cr”=“rc”<“snapshot”<“final “=”ga“<”sp“。所有这些知名的限定词被认为比其他字符串更小/更老。空的段/字符串等于0。
除了上述限定符之外,令牌“min”和“max”可以用作最终版本段,以表示具有给定前缀的最小/最大版本。例如,“1.2.min”表示1.2行中的最小版本,“1.2.max”表示1.2行中最大的版本。形式“[MN *]”的版本范围是“[MNmin,MNmax]”的缩写。
数字和字符串被认为是无法比拟的。在不同类型的版本段会相互冲突的情况下,比较将假定以前的段分别以0或“ga”段的形式进行填充,直到种类不一致被解决为止,例如“1-alpha”=“1.0.0-alpha “<”1.0.1-ga“=”1.0.1“。
Stub Runner用于消息传递
Stub Runner具有在内存中运行已发布存根的功能。它可以与开箱即用的以下框架集成
-
Spring Integration
-
Spring Cloud Stream
-
阿帕奇骆驼
-
Spring AMQP
它还提供了与市场上任何其他解决方案集成的入口点。
存根触发
要触发消息,只需使用StubTrigger
接口即可:
package org.springframework.cloud.contract.stubrunner;
import java.util.Collection;
import java.util.Map;
public interface StubTrigger {
/**
* Triggers an event by a given label for a given {@code groupid:artifactid} notation. You can use only {@code artifactId} too.
*
* Feature related to messaging.
*
* @return true - if managed to run a trigger
*/
boolean trigger(String ivyNotation, String labelName);
/**
* Triggers an event by a given label.
*
* Feature related to messaging.
*
* @return true - if managed to run a trigger
*/
boolean trigger(String labelName);
/**
* Triggers all possible events.
*
* Feature related to messaging.
*
* @return true - if managed to run a trigger
*/
boolean trigger();
/**
* Returns a mapping of ivy notation of a dependency to all the labels it has.
*
* Feature related to messaging.
*/
Map<String, Collection<String>> labels();
}
为了方便起见,StubFinder
接口扩展了StubTrigger
,所以只需要在你的测试中使用一个。
StubTrigger
提供以下选项来触发邮件:
按标签触发
stubFinder.trigger('return_book_1')
按组和人工制品ids触发
stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:camelService', 'return_book_1')
通过人工制品ids触发
stubFinder.trigger('camelService', 'return_book_1')
触发所有消息
stubFinder.trigger()
Stub Runner Camel
Spring Cloud Contract验证器Stub Runner的消息传递模块为您提供了与Apache Camel集成的简单方法。对于提供的工件,它将自动下载存根并注册所需的路由。
将其添加到项目中
在类路径上同时拥有Apache Camel和Spring Cloud Contract Stub Runner就足够了。记住用@AutoConfigureStubRunner
注释你的测试类。
例子
桩结构
让我们假设我们拥有以下Maven资源库,并为camelService
应用程序配置了一个存根。
└── .m2
└── repository
└── io
└── codearte
└── accurest
└── stubs
└── camelService
├── 0.0.1-SNAPSHOT
│ ├── camelService-0.0.1-SNAPSHOT.pom
│ ├── camelService-0.0.1-SNAPSHOT-stubs.jar
│ └── maven-metadata-local.xml
└── maven-metadata-local.xml
并且存根包含以下结构:
├── META-INF
│ └── MANIFEST.MF
└── repository
├── accurest
│ ├── bookDeleted.groovy
│ ├── bookReturned1.groovy
│ └── bookReturned2.groovy
└── mappings
让我们考虑以下合同(让我们用1来表示):
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('jms:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
和2号
Contract.make {
label 'return_book_2'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
情景1(无输入讯息)
为了通过return_book_1
标签触发一条消息,我们将使用StubTigger
接口,如下所示
stubFinder.trigger('return_book_1')
接下来我们将要收听发送到jms:output
的消息的输出。
Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)
接收到的消息将通过以下断言
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
情景2(输入触发输出)
由于路由是为您设置的,只需向jms:output
目的地发送消息即可。
camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])
接下来我们将要收听发送到jms:output
的消息的输出
Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)
接收到的消息将通过以下断言
receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
情景3(无输出输入)
由于路由是为您设置的,只需向jms:output
目的地发送消息即可。
camelContext.createProducerTemplate().sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])
Stub Runner整合
Spring Cloud Contract验证器Stub Runner的消息传递模块为您提供了一种简单的与Spring Integration集成的方法。对于提供的工件,它将自动下载存根并注册所需的路由。
将其添加到项目中
在类路径上同时拥有Spring Integration和Spring Cloud Contract Stub Runner就足够了。记住用@AutoConfigureStubRunner
注释你的测试类。
例子
桩结构
让我们假设我们拥有以下Maven仓库,并为integrationService
应用程序配置了一个存根。
└── .m2
└── repository
└── io
└── codearte
└── accurest
└── stubs
└── integrationService
├── 0.0.1-SNAPSHOT
│ ├── integrationService-0.0.1-SNAPSHOT.pom
│ ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
│ └── maven-metadata-local.xml
└── maven-metadata-local.xml
并且存根包含以下结构:
├── META-INF
│ └── MANIFEST.MF
└── repository
├── accurest
│ ├── bookDeleted.groovy
│ ├── bookReturned1.groovy
│ └── bookReturned2.groovy
└── mappings
让我们考虑以下合同(让我们用1来表示):
Contract.make {
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
和2号
Contract.make {
label 'return_book_2'
input {
messageFrom('input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
和以下Spring Integration路由:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd">
<!-- REQUIRED FOR TESTING -->
<bridge input-channel="output"
output-channel="outputTest"/>
<channel id="outputTest">
<queue/>
</channel>
</beans:beans>
情景1(无输入讯息)
为了通过return_book_1
标签触发消息,我们将使用StubTigger
接口,如下所示
stubFinder.trigger('return_book_1')
接下来我们将要收听发送到output
的消息的输出
Message<?> receivedMessage = messaging.receive('outputTest')
接收到的消息将通过以下断言
receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
情景2(输入触发输出)
由于路由是为您设置的,只需向output
目的地发送消息即可。
messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')
接下来,我们将要收听发送到output
的邮件的输出
Message<?> receivedMessage = messaging.receive('outputTest')
接收到的消息将通过以下断言
receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
情景3(无输出输入)
由于路由是为您设置的,只需向input
目的地发送一条消息即可。
messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')
Stub Runner Stream
Spring Cloud Contract验证器Stub Runner的消息传递模块为您提供了一种简单的与Spring Stream集成的方法。对于提供的工件,它将自动下载存根并注册所需的路由。
警告
|
在Stub Runner与Stream的集成中,messageFrom 或sentTo 字符串首先被解析为destination 的频道,然后如果没有这样的destination ,它被解析为频道名称。
|
重要
|
如果要使用Spring Cloud Stream记住添加org.springframework.cloud:spring-cloud-stream-test-support 依赖项。
|
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
将其添加到项目中
在类路径上同时拥有Spring Cloud Stream和Spring Cloud Contract Stub Runner就足够了。记住用@AutoConfigureStubRunner
注释你的测试类。
例子
桩结构
让我们假设我们拥有以下Maven资源库,并为streamService
应用程序配置了一个存根。
└── .m2
└── repository
└── io
└── codearte
└── accurest
└── stubs
└── streamService
├── 0.0.1-SNAPSHOT
│ ├── streamService-0.0.1-SNAPSHOT.pom
│ ├── streamService-0.0.1-SNAPSHOT-stubs.jar
│ └── maven-metadata-local.xml
└── maven-metadata-local.xml
并且存根包含以下结构:
├── META-INF
│ └── MANIFEST.MF
└── repository
├── accurest
│ ├── bookDeleted.groovy
│ ├── bookReturned1.groovy
│ └── bookReturned2.groovy
└── mappings
让我们考虑以下合同(让我们用1来表示):
Contract.make {
label 'return_book_1'
input { triggeredBy('bookReturnedTriggered()') }
outputMessage {
sentTo('returnBook')
body('''{ "bookName" : "foo" }''')
headers { header('BOOK-NAME', 'foo') }
}
}
和2号
Contract.make {
label 'return_book_2'
input {
messageFrom('bookStorage')
messageBody([
bookName: 'foo'
])
messageHeaders { header('sample', 'header') }
}
outputMessage {
sentTo('returnBook')
body([
bookName: 'foo'
])
headers { header('BOOK-NAME', 'foo') }
}
}
和以下Spring配置:
stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
spring:
cloud:
stream:
bindings:
output:
destination: returnBook
input:
destination: bookStorage
server:
port: 0
debug: true
情景1(无输入讯息)
为了通过return_book_1
标签触发消息,我们将使用StubTrigger
接口,如下所示
stubFinder.trigger('return_book_1')
接下来,我们要监听发送到destination
为returnBook
的频道的消息输出
Message<?> receivedMessage = messaging.receive('returnBook')
接收到的消息将通过以下断言
receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
情景2(输入触发输出)
由于路由是为您设置的,只需向bookStorage
destination
发送一条消息即可。
messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')
接下来,我们将要收听发送到returnBook
的消息的输出。
Message<?> receivedMessage = messaging.receive('returnBook')
接收到的消息将通过以下断言
receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
情景3(无输出输入)
由于路由是为您设置的,只需向output
目的地发送消息即可。
messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')
Stub Runner Spring AMQP
Spring Cloud Contract验证器Stub Runner的消息传递模块提供了一种简单的方法来与Spring AMQP的Rabbit模板集成。对于提供的工件,它将自动下载存根并注册所需的路由。
集成尝试独立运行,即不与运行的RabbitMQ消息代理交互。它希望在应用程序上下文中使用RabbitTemplate
,并将其用作spring boot测试@SpyBean
。因此,它可以使用mockito间谍功能来验证和内省应用程序发送的消息。
在消息消费者方面,它考虑了所有@RabbitListener
注释的端点以及应用程序上下文中的所有“SimpleMessageListenerContainer”。
由于消息通常发送到AMQP中的交换机,消息合同中包含交换机名称作为目标。另一方的消息侦听器绑定到队列。绑定将交换机连接到队列。如果触发消息合约,Spring AMQP存根转移器集成将在与该交换机匹配的应用程序上下文中查找绑定。然后它从Spring交换机收集队列,并尝试查找绑定到这些队列的消息侦听器。消息被触发到所有匹配的消息监听器。
将其添加到项目中
在类路径上同时拥有Spring AMQP和Spring Cloud Contract Stub Runner就足够了,并设置属性stubrunner.amqp.enabled=true
。请记住使用@AutoConfigureStubRunner
注释测试类。
例子
桩结构
让我们假设我们拥有以下Maven资源库,并为spring-cloud-contract-amqp-test
应用程序配置了一个存根。
└── .m2
└── repository
└── com
└── example
└── spring-cloud-contract-amqp-test
├── 0.4.0-SNAPSHOT
│ ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
│ ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
│ └── maven-metadata-local.xml
└── maven-metadata-local.xml
并且存根包含以下结构:
├── META-INF
│ └── MANIFEST.MF
└── contracts
└── shouldProduceValidPersonData.groovy
让我们考虑下列合约:
Contract.make {
// Human readable description
description 'Should produce valid person data'
// Label by means of which the output message can be triggered
label 'contract-test.person.created.event'
// input to the contract
input {
// the contract will be triggered by a method
triggeredBy('createPerson()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo 'contract-test.exchange'
headers {
header('contentType': 'application/json')
header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person')
}
// the body of the output message
body ([
id: $(consumer(9), producer(regex("[0-9]+"))),
name: "me"
])
}
}
和以下Spring配置:
stubrunner:
repositoryRoot: classpath:m2repo/repository/
ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs
amqp:
enabled: true
server:
port: 0
触发消息
因此,为了触发使用上述合同的消息,我们将使用StubTrigger
接口,如下所示。
stubTrigger.trigger("contract-test.person.created.event")
消息具有目的地contract-test.exchange
,所以Spring AMQP存根转移器集成查找与此交换相关的绑定。
@Bean
public Binding binding() {
return BindingBuilder.bind(new Queue("test.queue")).to(new DirectExchange("contract-test.exchange")).with("#");
}
绑定定义绑定队列test.queue
。因此,以下监听器定义是一个匹配,并使用合同消息进行调用。
@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer(ConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("test.queue");
container.setMessageListener(listenerAdapter);
return container;
}
此外,以下注释的监听器表示一个匹配并将被调用。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "test.queue"),
exchange = @Exchange(value = "contract-test.exchange", ignoreDeclarationExceptions = "true")))
public void handlePerson(Person person) {
this.person = person;
}
注意
|
该消息直接交给与匹配SimpleMessageListenerContainer 相关联的MessageListener 的onMessage 方法。
|
Spring AMQP测试配置
为了避免在我们的测试期间Spring AMQP尝试连接到运行的代理,我们配置了一个模拟ConnectionFactory
。
要禁用嘲弄的ConnectionFactory设置属性stubrunner.amqp.mockConnection=false
stubrunner:
amqp:
mockConnection: false
Contract DSL
重要
|
请记住,在合同文件中,您必须向Contract 类和make 静态导入ie org.springframework.cloud.spec.Contract.make { … } 提供完全限定名称。您还可以向Contract 类import org.springframework.cloud.spec.Contract 提供导入,然后调用Contract.make { … }
|
Contract DSL写在Groovy中,但如果以前没有使用Groovy,不要惊慌。语言的知识并不是真正需要的,因为我们的DSL只使用它的一小部分(即文字,方法调用和闭包)。DSL还被设计为程序员可读,而不需要DSL本身的知识 - 它是静态类型的。
提示
|
Spring Cloud Contract支持在单个文件中定义多个合同! |
合同存在于Spring Cloud Contract验证程序存储库的spring-cloud-contract-spec
模块中。
我们来看一下合同定义的完整例子。
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/api/12'
headers {
header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json'
}
body '''\
[{
"created_at": "Sat Jul 26 09:38:57 +0000 2014",
"id": 492967299297845248,
"id_str": "492967299297845248",
"text": "Gonna see you at Warsaw",
"place":
{
"attributes":{},
"bounding_box":
{
"coordinates":
[[
[-77.119759,38.791645],
[-76.909393,38.791645],
[-76.909393,38.995548],
[-77.119759,38.995548]
]],
"type":"Polygon"
},
"country":"United States",
"country_code":"US",
"full_name":"Washington, DC",
"id":"01fbe706f872cb32",
"name":"Washington",
"place_type":"city",
"url": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
}
}]
'''
}
response {
status 200
}
}
不是DSL的所有功能都在上面的例子中使用。如果您找不到您想要的内容,请查看本页下面的段落。
您可以使用独立的maven命令
mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert
轻松地将Contracts编译为WireMock存根映射。
限制
警告
|
Spring Cloud Contract验证器不正确支持XML。请使用JSON或帮助我们实现此功能。 |
警告
|
对JSON数组的大小的验证的支持是实验性的。如果要打开它,请提供等于true 的系统属性spring.cloud.contract.verifier.assert.size 的值。默认情况下,此功能设置为false 。您还可以在插件配置中提供assertJsonSize 属性。
|
警告
|
由于JSON结构可以有任何形式,因此在GString中使用时使用value(consumer(…), producer(…)) 符号时,有时无法正确解析它。这就是为什么我们强烈推荐使用Groovy地图符号。
|
常见的顶级元素
描述
您可以在合同中添加一个description
,而不是任意文本。例:
org.springframework.cloud.contract.spec.Contract.make {
description('''
given:
An input
when:
Sth happens
then:
Output
''')
}
名称
您可以提供您的合同名称。假设你提供了一个名字should register a user
。如果这样做,则自动生成测试的名称将等于validate_should_register_a_user
。另外,如果是WireMock存根,存根的名称也就是should_register_a_user.json
。
重要
|
请确保该名称不包含任何会使生成的测试无法编译的字符。还要记住,如果您为多个合同提供相同的名称,那么您的自动生成测试将无法编译,并且生成的存根将相互覆盖。 |
忽略合同
如果您想忽略合同,您可以在插件配置中设置被忽略的合同值,或者仅在合同本身设置ignored
属性:
org.springframework.cloud.contract.spec.Contract.make {
ignored()
}
HTTP顶级元素
可以在合同定义的顶层关闭中调用以下方法。请求和响应是强制性的,优先级是可选的。
org.springframework.cloud.contract.spec.Contract.make {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
request {
//...
}
// Definition of HTTP response part of the contract
// (a service implementing this contract should respond
// with following response after receiving request
// specified in "request" part above).
response {
//...
}
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
priority 1
}
请求
HTTP协议只需要在请求中指定方法和地址。在合同的请求定义中,相同的信息是强制性的。
org.springframework.cloud.contract.spec.Contract.make {
request {
// HTTP request method (GET/POST/PUT/DELETE).
method 'GET'
// Path component of request URL is specified as follows.
urlPath('/users')
}
response {
//...
}
}
可以指定整个url
而不是路径,但是urlPath
是测试与主机无关的推荐方法。
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
// Specifying `url` and `urlPath` in one contract is illegal.
url('http://localhost:8888/users')
}
response {
//...
}
}
请求可能包含查询参数,这些参数在嵌套在urlPath
或url
的调用中的闭包中指定。
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
urlPath('/users') {
// Each parameter is specified in form
// `'paramName' : paramValue` where parameter value
// may be a simple literal or one of matcher functions,
// all of which are used in this example.
queryParameters {
// If a simple literal is used as value
// default matcher function is used (equalTo)
parameter 'limit': 100
// `equalTo` function simply compares passed value
// using identity operator (==).
parameter 'filter': equalTo("email")
// `containing` function matches strings
// that contains passed substring.
parameter 'gender': value(consumer(containing("[mf]")), producer('mf'))
// `matching` function tests parameter
// against passed regular expression.
parameter 'offset': value(consumer(matching("[0-9]+")), producer(123))
// `notMatching` functions tests if parameter
// does not match passed regular expression.
parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3))
}
}
//...
}
response {
//...
}
}
它可能包含其他请求标头 ...
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
headers {
header 'key': 'value'
contentType(applicationJson())
}
//...
}
response {
//...
}
}
...和请求机构。
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
// Currently only JSON format of request body is supported.
// Format will be determined from a header or body's content.
body '''{ "login" : "john", "name": "John The Contract" }'''
}
response {
//...
}
}
请求可能包含多部分元素。只需调用multipart()
方法即可。
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
// key (parameter name), value (parameter value) pair
formParameter: $(c(regex('".+"')), p('"formParameterValue"')),
someBooleanParameter: $(c(regex(anyBoolean())), p('true')),
// a named parameter (e.g. with `file` name) that represents file with
// `name` and `content`. You can also call `named("fileName", "fileContent")`
file: named(
// name of the file
name: $(c(regex(nonEmpty())), p('filename.csv')),
// content of the file
content: $(c(regex(nonEmpty())), p('file content')))
)
}
response {
status 200
}
}
在这个例子中,我们通过使用地图符号直接定义参数,其中值可以是动态属性(例如formParameter: $(consumer(…), producer(…))
),或者使用允许您设置命名参数的named(…)
方法。命名参数可以设置name
和content
。您可以通过具有2个参数的方法来调用它:例如named("fileName", "fileContent")
或通过地图符号named(name: "fileName", content: "fileContent")
。
从这个合同中,生成的测试或多或少会看起来像这样:
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "multipart/form-data;boundary=AaB03x")
.param("formParameter", "\"formParameterValue\"")
.param("someBooleanParameter", "true")
.multiPart("file", "filename.csv", "file content".getBytes());
// when:
ResponseOptions response = given().spec(request)
.put("/multipart");
// then:
assertThat(response.statusCode()).isEqualTo(200);
WireMock存根或多或少会看起来像这样:
'''
{
"request" : {
"url" : "/multipart",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"matches" : "multipart/form-data;boundary=AaB03x.*"
}
},
"bodyPatterns" : [ {
"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*"
}, {
"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*"
}, {
"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\".+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n.+\\r\\n--\\\\1.*"
} ]
},
"response" : {
"status" : 200,
"transformers" : [ "response-template" ]
}
}
'''
响应
最小响应必须包含HTTP状态代码。
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
}
response {
// Status code sent by the server
// in response to request specified above.
status 200
}
}
除了状态响应可能包含标头和正文之外,它们与请求中的方式相同(参见前一段)。
动态属性
该合同可以包含一些动态属性 - 时间戳/ ids等。您不想强制使用者将其时钟保留为始终返回相同的时间值,以使其与存根匹配。这就是为什么我们允许您以两种方式在合同中提供动态部分。一个是将它们直接传递到身体,另一个是将它们设置在另一个名为testMatchers
和stubMatchers
的部分中。
体内动态属性
您可以通过value
方法设置正文内的属性
value(consumer(...), producer(...))
value(c(...), p(...))
value(stub(...), test(...))
value(client(...), server(...))
或者如果您正在使用Groovy地图符号,您可以使用$()
方法
$(consumer(...), producer(...))
$(c(...), p(...))
$(stub(...), test(...))
$(client(...), server(...))
所有上述方法都是相同的。这意味着stub
和client
方法是consumer
方法的别名。我们来仔细看看我们可以在后续章节中对这些值做些什么。
正则表达式
您可以使用正则表达式将您的请求写入Contract DSL。当您想要指出给定的响应应该被提供给遵循给定模式的请求时,这是特别有用的。此外,当您需要使用模式,而不是测试和服务器端测试时,您可以使用它。
请看下面的例子:
org.springframework.cloud.contract.spec.Contract.make {
request {
method('GET')
url $(consumer(~/\/[0-9]{2}/), producer('/12'))
}
response {
status 200
body(
id: $(anyNumber()),
surname: $(
consumer('Kowalsky'),
producer(regex('[a-zA-Z]+'))
),
name: 'Jan',
created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))),
correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
producer(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
)
)
headers {
header 'Content-Type': 'text/plain'
}
}
}
您还可以使用正则表达式仅提供通信的一方。如果这样做,那么我们将自动提供与提供的正则表达式匹配的生成的字符串。例如:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url value(consumer(regex('/foo/[0-9]{5}')))
body([
requestElement: $(consumer(regex('[0-9]{5}')))
])
headers {
header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*'))))
}
}
response {
status 200
body([
responseElement: $(producer(regex('[0-9]{7}')))
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}
在该示例中,对于请求和响应,通信的相对侧将具有生成的相应数据。
Spring Cloud Contract附有一系列预定义的正则表达式,您可以在合同中使用。
protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/)
protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/)
protected static final Pattern NUMBER = Pattern.compile('-?\\d*(\\.\\d+)?')
protected static final Pattern IP_ADDRESS = Pattern.compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])')
protected static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):/)/?([^:/\\s]+)(:[0-9]{1,5})?')
protected static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}')
protected static final Pattern URL = UrlHelper.URL
protected static final Pattern UUID = Pattern.compile('[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}')
protected static final Pattern ANY_DATE = Pattern.compile('(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])')
protected static final Pattern ANY_DATE_TIME = Pattern.compile('([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
protected static final Pattern ANY_TIME = Pattern.compile('(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
protected static final Pattern NON_EMPTY = Pattern.compile(/.+/)
protected static final Pattern NON_BLANK = Pattern.compile(/.*(\S+|\R).*|!^\R*$/)
protected static final Pattern ISO8601_WITH_OFFSET = Pattern.compile(/([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.\d{3})?(Z|[+-][01]\d:[0-5]\d)/)
protected static Pattern anyOf(String... values){
return Pattern.compile(values.collect({"^$it\$"}).join("|"))
}
String onlyAlphaUnicode() {
return ONLY_ALPHA_UNICODE.pattern()
}
String number() {
return NUMBER.pattern()
}
String anyBoolean() {
return TRUE_OR_FALSE.pattern()
}
String ipAddress() {
return IP_ADDRESS.pattern()
}
String hostname() {
return HOSTNAME_PATTERN.pattern()
}
String email() {
return EMAIL.pattern()
}
String url() {
return URL.pattern()
}
String uuid(){
return UUID.pattern()
}
String isoDate() {
return ANY_DATE.pattern()
}
String isoDateTime() {
return ANY_DATE_TIME.pattern()
}
String isoTime() {
return ANY_TIME.pattern()
}
String iso8601WithOffset() {
return ISO8601_WITH_OFFSET.pattern()
}
String nonEmpty() {
return NON_EMPTY.pattern()
}
String nonBlank() {
return NON_BLANK.pattern()
}
所以在你的合同中你可以这样使用它
Contract dslWithOptionalsInString = Contract.make {
priority 1
request {
method POST()
url '/users/password'
headers {
contentType(applicationJson())
}
body(
email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
)
}
response {
status 404
headers {
contentType(applicationJson())
}
body(
code: value(consumer("123123"), producer(optional("123123"))),
message: "User not found by email = [${value(producer(regex(email())), consumer('not.existing@user.com'))}]"
)
}
}
传递可选参数
可以在您的合同中提供可选参数。只能有可选参数:
-
STUB侧的请求
-
响应的TEST侧
例:
org.springframework.cloud.contract.spec.Contract.make {
priority 1
request {
method 'POST'
url '/users/password'
headers {
contentType(applicationJson())
}
body(
email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
)
}
response {
status 404
headers {
header 'Content-Type': 'application/json'
}
body(
code: value(consumer("123123"), producer(optional("123123")))
)
}
}
通过使用optional()
方法包装身体的一部分,您实际上正在创建一个应该存在0次或更多次的正则表达式。
如果您选择Spock,那么上述示例将会生成以下测试:
"""
given:
def request = given()
.header("Content-Type", "application/json")
.body('''{"email":"abc@abc.com","callback_url":"http://partners.com"}''')
when:
def response = given().spec(request)
.post("/users/password")
then:
response.statusCode == 404
response.header('Content-Type') == 'application/json'
and:
DocumentContext parsedJson = JsonPath.parse(response.body.asString())
assertThatJson(parsedJson).field("['code']").matches("(123123)?")
"""
和以下存根:
'''
{
"request" : {
"url" : "/users/password",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})?/)]"
}, {
"matchesJsonPath" : "$[?(@.['callback_url'] =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
} ],
"headers" : {
"Content-Type" : {
"equalTo" : "application/json"
}
}
},
"response" : {
"status" : 404,
"body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}",
"headers" : {
"Content-Type" : "application/json"
}
},
"priority" : 1
}
'''
在服务器端执行自定义方法
也可以在测试期间定义要在服务器端执行的方法调用。这样的方法可以添加到在配置中定义为“baseClassForTests”的类中。例:
合同
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url $(consumer(regex('^/api/[0-9]{2}$')), producer('/api/12'))
headers {
header 'Content-Type': 'application/json'
}
body '''\
[{
"text": "Gonna see you at Warsaw"
}]
'''
}
response {
body (
path: $(consumer('/api/12'), producer(regex('^/api/[0-9]{2}$'))),
correlationId: $(consumer('1223456'), producer(execute('isProperCorrelationId($it)')))
)
status 200
}
}
基础班
abstract class BaseMockMvcSpec extends Specification {
def setup() {
RestAssuredMockMvc.standaloneSetup(new PairIdController())
}
void isProperCorrelationId(Integer correlationId) {
assert correlationId == 123456
}
void isEmpty(String value) {
assert value == null
}
}
重要
|
您不能同时使用String和execute 来执行连接。例如电话header('Authorization', 'Bearer ' + execute('authToken()')) 将导致不正确的结果。要使此工作只需调用header('Authorization', execute('authToken()')) ,并确保authToken() 方法返回您需要的所有内容。
|
从JSON读取的对象的类型可以是以下之一,具体取决于JSON路径:
-
String
如果您指向JSON中的String
值 -
JSONArray
如果你指向一个List
在一个JSON -
Map
如果你指向一个Map
在一个JSON -
如果您在JSON中指向
Integer
,Double
等,则Number
正确 -
Boolean
如果你指向一个Boolean
在一个JSON
在合同的请求部分,您可以指定body
应从方法中获取。
重要
|
您必须提供消费者和生产者方面,execute 部分可以应用于全身。不是为了它的一部分!
|
例:
Contract contractDsl = Contract.make {
request {
method 'GET'
url '/something'
body(
$(c("foo"), p(execute("hashCode()")))
)
}
response {
status 200
}
}
这将导致在请求正文中调用hashCode()
方法。它或多或少是这样的:
// given:
MockMvcRequestSpecification request = given()
.body(hashCode());
// when:
ResponseOptions response = given().spec(request)
.get("/something");
// then:
assertThat(response.statusCode()).isEqualTo(200);
从响应引用请求
最好的情况是提供固定值,但有时您需要在响应中引用请求。为了做到这一点,您可以从fromRequest()
方法中获利,从而允许您从HTTP请求中引用一堆元素。您可以使用以下选项:
-
fromRequest().url()
- 返回请求URL -
fromRequest().query(String key)
- 返回具有给定名称的第一个查询参数 -
fromRequest().query(String key, int index)
- 返回具有给定名称的第n个查询参数 -
fromRequest().header(String key)
- 返回具有给定名称的第一个标题 -
fromRequest().header(String key, int index)
- 返回具有给定名称的第n个标题 -
fromRequest().body()
- 返回完整的请求体 -
fromRequest().body(String jsonPath)
- 从与JSON路径匹配的请求中返回元素
我们来看看下面的合同
Contract contractDsl = Contract.make {
request {
method 'GET'
url('/api/v1/xxxx') {
queryParameters {
parameter("foo", "bar")
parameter("foo", "bar2")
}
}
headers {
header(authorization(), "secret")
header(authorization(), "secret2")
}
body(foo: "bar", baz: 5)
}
response {
status 200
headers {
header(authorization(), "foo ${fromRequest().header(authorization())} bar")
}
body(
url: fromRequest().url(),
param: fromRequest().query("foo"),
paramIndex: fromRequest().query("foo", 1),
authorization: fromRequest().header("Authorization"),
authorization2: fromRequest().header("Authorization", 1),
fullBody: fromRequest().body(),
responseFoo: fromRequest().body('$.foo'),
responseBaz: fromRequest().body('$.baz'),
responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla"
)
}
}
运行JUnit测试代码将导致创建一个或多或少这样的测试
// given:
MockMvcRequestSpecification request = given()
.header("Authorization", "secret")
.header("Authorization", "secret2")
.body("{\"foo\":\"bar\",\"baz\":5}");
// when:
ResponseOptions response = given().spec(request)
.queryParam("foo","bar")
.queryParam("foo","bar2")
.get("/api/v1/xxxx");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Authorization")).isEqualTo("foo secret bar");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("url").isEqualTo("/api/v1/xxxx");
assertThatJson(parsedJson).field("fullBody").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
assertThatJson(parsedJson).field("paramIndex").isEqualTo("bar2");
assertThatJson(parsedJson).field("responseFoo").isEqualTo("bar");
assertThatJson(parsedJson).field("authorization2").isEqualTo("secret2");
assertThatJson(parsedJson).field("responseBaz").isEqualTo(5);
assertThatJson(parsedJson).field("responseBaz2").isEqualTo("Bla bla bar bla bla");
assertThatJson(parsedJson).field("param").isEqualTo("bar");
assertThatJson(parsedJson).field("authorization").isEqualTo("secret");
您可以看到请求中的元素在响应中已被正确引用。
生成的WireMock存根将看起来或多或少是这样的:
{
"request" : {
"urlPath" : "/api/v1/xxxx",
"method" : "POST",
"headers" : {
"Authorization" : {
"equalTo" : "secret2"
}
},
"queryParameters" : {
"foo" : {
"equalTo" : "bar2"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.baz == 5)]"
}, {
"matchesJsonPath" : "$[?(@.foo == 'bar')]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\"url\":\"{{{request.url}}}\",\"param\":\"{{{request.query.foo.[0]}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\",\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\"}",
"headers" : {
"Authorization" : "{{{request.headers.Authorization.[0]}}}"
},
"transformers" : [ "response-template" ]
}
}
因此,发送请求作为合同request
部分提出的请求将导致发送以下回复机构
{
"url" : "/api/v1/xxxx?foo=bar&foo=bar2",
"param" : "bar",
"paramIndex" : "bar2",
"authorization" : "secret",
"authorization2" : "secret2",
"fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
"responseFoo" : "bar",
"responseBaz" : 5,
"responseBaz2" : "Bla bla bar bla bla"
}
重要
|
此功能仅适用于版本大于或等于2.5.1的WireMock。我们正在使用WireMock的response-template 响应变压器。它使用Handlebars将Mustache {{{ }}} 模板转换成适当的值。另外我们正在注册2个帮助函数。escapejsonbody - 以可嵌入JSON的格式转义请求正文。另一个是jsonpath 对于给定的参数知道如何在请求体中找到一个对象。
|
匹配部分的动态属性
如果您一直在使用Pact,这似乎很熟悉。很多用户习惯于在身体和设定合约的动态部分之间进行分隔。
这就是为什么你可以从两个不同的部分获利。一个称为stubMatchers
,您可以在其中定义应该存在于存根中的动态值。您可以在合同的request
或inputMessage
部分设置。另一个被称为testMatchers
,它存在于合同的response
或outputMessage
方面。
目前,我们仅支持具有以下匹配可能性的基于JSON路径的匹配器。对于stubMatchers
:
-
byEquality()
- 通过提供的JSON路径从响应中获取的值需要等于合同中提供的值 -
byRegex(…)
- 通过提供的JSON路径从响应中获取的值需要与正则表达式匹配 -
byDate()
- 通过提供的JSON路径从响应中获取的值需要与ISO Date的正则表达式匹配 -
byTimestamp()
- 通过提供的JSON路径从响应中获取的值需要匹配ISO DateTime的正则表达式 -
byTime()
- 通过提供的JSON路径从响应中获取的值需要与ISO时间的正则表达式匹配
对于testMatchers
:
-
byEquality()
- 通过提供的JSON路径从响应中获取的值需要等于合同中提供的值 -
byRegex(…)
- 通过提供的JSON路径从响应中获取的值需要与正则表达式匹配 -
byDate()
- 通过提供的JSON路径从响应中获取的值需要与ISO Date的正则表达式匹配 -
byTimestamp()
- 通过提供的JSON路径从响应中获取的值需要与ISO DateTime的正则表达式匹配 -
byTime()
- 通过提供的JSON路径从响应中获取的值需要匹配ISO时间的正则表达式 -
byType()
- 通过提供的JSON路径从响应中获取的值必须与合同中的响应正文中定义的类型相同。byType
可以关闭,您可以设置minOccurrence
和maxOccurrence
。这样你可以断定集合的大小。 -
byCommand(…)
- 通过提供的JSON路径从响应中获取的值将作为输入传递给您提供的自定义方法。例如byCommand('foo($it)')
将导致调用与JSON路径匹配的值将被通过的foo
方法。-
从JSON读取的对象的类型可以是以下之一,具体取决于JSON路径:
-
如果您指向JSON中的
String
值,则为String
-
JSONArray
如果你指向一个List
在一个JSON -
Map
如果你指向一个Map
在一个JSON -
如果您在JSON中指向
Integer
,Double
等等,那么Number
正确 -
Boolean
如果你指向一个Boolean
在一个JSON
-
-
我们来看看下面的例子:
Contract contractDsl = Contract.make {
request {
method 'GET'
urlPath '/get'
body([
duck: 123,
alpha: "abc",
number: 123,
aBoolean: true,
date: "2017-01-01",
dateTime: "2017-01-01T01:23:45",
time: "01:02:34",
valueWithoutAMatcher: "foo",
valueWithTypeMatch: "string",
key: [
'complex.key' : 'foo'
]
])
stubMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body([
duck: 123,
alpha: "abc",
number: 123,
aBoolean: true,
date: "2017-01-01",
dateTime: "2017-01-01T01:23:45",
time: "01:02:34",
valueWithoutAMatcher: "foo",
valueWithTypeMatch: "string",
valueWithMin: [
1,2,3
],
valueWithMax: [
1,2,3
],
valueWithMinMax: [
1,2,3
],
valueWithMinEmpty: [],
valueWithMaxEmpty: [],
key: [
'complex.key' : 'foo'
]
])
testMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}"))
// asserts the jsonpath value against the provided value
jsonPath('$.duck', byEquality())
// asserts the jsonpath value against some default regex
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
// asserts vs inbuilt time related regex
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
// asserts that the resulting type is the same as in response body
jsonPath('$.valueWithTypeMatch', byType())
jsonPath('$.valueWithMin', byType {
// results in verification of size of array (min 1)
minOccurrence(1)
})
jsonPath('$.valueWithMax', byType {
// results in verification of size of array (max 3)
maxOccurrence(3)
})
jsonPath('$.valueWithMinMax', byType {
// results in verification of size of array (min 1 & max 3)
minOccurrence(1)
maxOccurrence(3)
})
jsonPath('$.valueWithMinEmpty', byType {
// results in verification of size of array (min 0)
minOccurrence(0)
})
jsonPath('$.valueWithMaxEmpty', byType {
// results in verification of size of array (max 0)
maxOccurrence(0)
})
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
}
在这个例子中,我们在匹配器部分提供合同的动态部分。对于请求部分,您可以看到对于所有字段,但是valueWithoutAMatcher
我们正在明确地设置我们希望存根包含的正则表达式的值。对于valueWithoutAMatcher
,验证将以与不使用匹配器相同的方式进行 - 在这种情况下,测试将执行相等检查。
对于testMatchers
部分的响应方面,我们以类似的方式定义所有的动态部分。唯一的区别是我们也有byType
匹配器。在这种情况下,我们正在检查4个字段,我们正在验证测试的响应是否具有一个值,其JSON路径与给定字段匹配的类型与响应主体中定义的相同,
-
对于
$.valueWithTypeMatch
- 我们只是检查类型是否相同 -
对于
$.valueWithMin
- 我们正在检查类型,并声明大小是否大于或等于最小出现次数 -
对于
$.valueWithMax
- 我们正在检查类型,并声明大小是否小于或等于最大出现次数 -
对于
$.valueWithMinMax
- 我们正在检查类型并声明大小是否在最小和最大值之间
所得到的测试或多或少会看起来像这样(请注意,我们将自动生成的断言与匹配器与and
部分分开):
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\"}");
// when:
ResponseOptions response = given().spec(request)
.get("/get");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo");
// and:
assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?");
assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class)).hasSizeLessThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).hasSizeBetween(1, 3);
assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(0);
assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).hasSizeLessThanOrEqualTo(0);
assertThatValueIsANumber(parsedJson.read("$.duck"));
和WireMock这样的stub:
'''
{
"request" : {
"urlPath" : "/get",
"method" : "POST",
"headers" : {
"Content-Type" : {
"matches" : "application/json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]"
}, {
"matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]"
}, {
"matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]"
}, {
"matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]"
}, {
"matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]"
}, {
"matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
}, {
"matchesJsonPath" : "$[?(@.duck == 123)]"
}, {
"matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
}, {
"matchesJsonPath" : "$[?(@.alpha == 'abc')]"
}, {
"matchesJsonPath" : "$[?(@.number =~ /(-?\\\\d*(\\\\.\\\\d+)?)/)]"
}, {
"matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]"
}, {
"matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]"
}, {
"matchesJsonPath" : "$[?(@.dateTime =~ /(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
}, {
"matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
}, {
"matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"number\\":123,\\"aBoolean\\":true,\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"time\\":\\"01:02:34\\",\\"valueWithoutAMatcher\\":\\"foo\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3]}",
"headers" : {
"Content-Type" : "application/json"
}
}
}
'''
JAX-RS支持
我们支持JAX-RS 2 Client API。基类需要定义protected WebTarget webTarget
和服务器初始化,现在唯一的选择如何测试JAX-RS API是启动web服务器。
使用身体的请求需要设置内容类型,否则将使用application/octet-stream
。
为了使用JAX-RS模式,请使用以下设置:
testMode === 'JAXRSCLIENT'
生成测试API的示例:
'''
// when:
Response response = webTarget
.path("/users")
.queryParam("limit", "10")
.queryParam("offset", "20")
.queryParam("filter", "email")
.queryParam("sort", "name")
.queryParam("search", "55")
.queryParam("age", "99")
.queryParam("name", "Denis.Stepanov")
.queryParam("email", "bob@email.com")
.request()
.method("GET");
String responseAsString = response.readEntity(String.class);
// then:
assertThat(response.getStatus()).isEqualTo(200);
// and:
DocumentContext parsedJson = JsonPath.parse(responseAsString);
assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
'''
异步支持
如果您在服务器端使用异步通信(您的控制器正在返回Callable
,DeferredResult
等等,然后在合同中您必须在response
部分中提供async()
方法。 :
org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url '/get'
}
response {
status 200
body 'Passed'
async()
}
}
使用上下文路径
Spring Cloud Contract支持上下文路径。
重要
|
为了完全支持上下文路径,唯一改变的是在PRODUCER端的切换。自动生成测试需要使用EXPLICIT模式。 |
消费者方面保持不变,为了让生成的测试通过,您必须切换EXPLICIT模式。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>EXPLICIT</testMode>
</configuration>
</plugin>
这样就可以生成不使用MockMvc 的测试。这意味着您正在生成真实的请求,您需要设置生成的测试的基类以在真正的套接字上工作。
让我们想象下面的合同:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/my-context-path/url'
}
response {
status 200
}
}
以下是一个如何设置基类和Rest Assured的示例,以使所有操作都正常工作。
import com.jayway.restassured.RestAssured;
import org.junit.Before;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ContextPathTestingBaseClass {
@LocalServerPort int port;
@Before
public void setup() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = this.port;
}
}
这样一来:
-
您自动生成测试中的所有请求将被发送到包含上下文路径的实际端点(例如
/my-context-path/url
) -
您的合同反映您具有上下文路径,因此您生成的存根也将具有该信息(例如,在存根中您将看到您也已经调用
/my-context-path/url
)
消息传递顶级元素
消息传递的DSL与重点在HTTP上的DSL有点不同。
由方法触发的输出
可以通过调用方法来触发输出消息(例如,调度程序启动并发送消息)
def dsl = Contract.make {
// Human readable description
description 'Some description'
// Label by means of which the output message can be triggered
label 'some_label'
// input to the contract
input {
// the contract will be triggered by a method
triggeredBy('bookReturnedTriggered()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo('output')
// the body of the output message
body('''{ "bookName" : "foo" }''')
// the headers of the output message
headers {
header('BOOK-NAME', 'foo')
}
}
}
在这种情况下,如果将执行一个名为bookReturnedTriggered
的方法,输出消息将被发送到output
。在消息发布者的一方,我们将生成一个测试,该测试将调用该方法来触发该消息。在消费者端,您可以使用some_label
触发消息。
由消息触发的输出
可以通过接收消息来触发输出消息。
def dsl = Contract.make {
description 'Some Description'
label 'some_label'
// input is a message
input {
// the message was received from this destination
messageFrom('input')
// has the following body
messageBody([
bookName: 'foo'
])
// and the following headers
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
在这种情况下,如果input
目的地收到正确的消息,则输出消息将发送到output
。在消息发布者的一方,我们将生成一个测试,它将输入消息发送到定义的目的地。在消费者端,您可以向输入目的地发送消息,也可以使用some_label
触发消息。
消费者/生产者
在HTTP中,您有一个client
/ stub and `server
/ test
符号的概念。您也可以在消息中使用它们,但是我们还提供以下所示的consumer
和produer
方法(请注意,您可以使用$
或value
方法提供consumer
和producer
部分)
Contract.make {
label 'some_label'
input {
messageFrom value(consumer('jms:output'), producer('jms:input'))
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo $(consumer('jms:input'), producer('jms:output'))
body([
bookName: 'foo'
])
}
}
一个文件中的多个合同
可以在一个文件中定义多个合同。这样的合同的例子可以这样看
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
name("should post a user")
request {
method 'POST'
url('/users/1')
}
response {
status 200
}
},
Contract.make {
request {
method 'POST'
url('/users/2')
}
response {
status 200
}
}
]
在这个例子中,一个合同有name
字段,另一个没有。这将导致生成两个或多或少这样的测试:
package org.springframework.cloud.contract.verifier.tests.com.hello;
import com.example.TestBase;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import com.jayway.restassured.response.ResponseOptions;
import org.junit.Test;
import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
public class V1Test extends TestBase {
@Test
public void validate_should_post_a_user() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.post("/users/1");
// then:
assertThat(response.statusCode()).isEqualTo(200);
}
@Test
public void validate_withList_1() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.post("/users/2");
// then:
assertThat(response.statusCode()).isEqualTo(200);
}
}
请注意,对于具有name
字段的合同,生成的测试方法名为validate_should_post_a_user
。对于那个没有名称的人validate_withList_1
。它对应于文件WithList.groovy
的名称和列表中的合同索引。
生成的存根将看起来像这样
should post a user.json
1_WithList.json
您可以看到第一个文件从合同中获取了name
参数。第二个获得了以索引为前缀的合同文件WithList.groovy
的名称(在这种情况下,合同在文件中的合同列表中具有索引1
)。
提示
|
正如你可以看到,如果您的合同名称更好,那么您的测试更有意义。 |
定制
扩展DSL
可以向DSL提供自己的功能。此功能的关键要求是保持静态兼容性。下面你可以看到一个例子:
-
创建具有可重用类的JAR
-
在DSL中引用这些类
这里可以找到完整的例子。
普通JAR
下面你可以找到我们将在DSL中重用的三个类。
PatternUtils包含消费者和制作者使用的功能。
package com.example;
import java.util.regex.Pattern;
/**
* If you want to use {@link Pattern} directly in your tests
* then you can create a class resembling this one. It can
* contain all the {@link Pattern} you want to use in the DSL.
*
* <pre>
* {@code
* request {
* body(
* [ age: $(c(PatternUtils.oldEnough()))]
* )
* }
* </pre>
*
* Notice that we're using both {@code $()} for dynamic values
* and {@code c()} for the consumer side.
*
* @author Marcin Grzejszczak
*/
//tag::impl[]
public class PatternUtils {
public static String tooYoung() {
//remove::start[]
return "[0-1][0-9]";
//remove::end[return]
}
public static Pattern oldEnough() {
//remove::start[]
return Pattern.compile("[2-9][0-9]");
//remove::end[return]
}
/**
* Makes little sense but it's just an example ;)
*/
public static Pattern ok() {
//remove::start[]
return Pattern.compile("OK");
//remove::end[return]
}
}
//end::impl[]
ConsumerUtils包含由使用功能的消费者。
package com.example;
import org.springframework.cloud.contract.spec.internal.ClientDslProperty;
/**
* DSL Properties passed to the DSL from the consumer's perspective.
* That means that on the input side {@code Request} for HTTP
* or {@code Input} for messaging you can have a regular expression.
* On the {@code Response} for HTTP or {@code Output} for messaging
* you have to have a concrete value.
*
* @author Marcin Grzejszczak
*/
//tag::impl[]
public class ConsumerUtils {
/**
* Consumer side property. By using the {@link ClientDslProperty}
* you can omit most of boilerplate code from the perspective
* of dynamic values. Example
*
* <pre>
* {@code
* request {
* body(
* [ age: $(ConsumerUtils.oldEnough())]
* )
* }
* </pre>
*
* That way it's in the implementation that we decide what value we will pass to the consumer
* and which one to the producer.
*
* @author Marcin Grzejszczak
*/
public static ClientDslProperty oldEnough() {
//remove::start[]
// this example is not the best one and
// theoretically you could just pass the regex instead of `ServerDslProperty` but
// it's just to show some new tricks :)
return new ClientDslProperty(PatternUtils.oldEnough(), 40);
//remove::end[return]
}
}
//end::impl[]
ProducerUtils包含由使用的功能制片人。
package com.example;
import org.springframework.cloud.contract.spec.internal.ServerDslProperty;
/**
* DSL Properties passed to the DSL from the producer's perspective.
* That means that on the input side {@code Request} for HTTP
* or {@code Input} for messaging you have to have a concrete value.
* On the {@code Response} for HTTP or {@code Output} for messaging
* you can have a regular expression.
*
* @author Marcin Grzejszczak
*/
//tag::impl[]
public class ProducerUtils {
/**
* Producer side property. By using the {@link ProducerUtils}
* you can omit most of boilerplate code from the perspective
* of dynamic values. Example
*
* <pre>
* {@code
* response {
* body(
* [ status: $(ProducerUtils.ok())]
* )
* }
* </pre>
*
* That way it's in the implementation that we decide what value we will pass to the consumer
* and which one to the producer.
*/
public static ServerDslProperty ok() {
// this example is not the best one and
// theoretically you could just pass the regex instead of `ServerDslProperty` but
// it's just to show some new tricks :)
return new ServerDslProperty( PatternUtils.ok(), "OK");
}
}
//end::impl[]
将依赖项添加到项目中
为了使插件和IDE能够引用常见的JAR类,您需要将依赖关系传递给您的项目。
测试依赖项目的依赖关系
首先将常见的jar依赖项添加为测试依赖关系。这样,由于您的合同文件在测试资源路径中可用,因此,您的Groovy文件中会自动显示常见的jar类。
<dependency>
<groupId>com.example</groupId>
<artifactId>beer-common</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
测试插件依赖关系
现在你必须添加插件的依赖关系,以便在运行时重用。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example</packageWithBaseClasses>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*intoxication.*</contractPackageRegex>
<baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>beer-common</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</plugin>
在DSL中引用类
现在您可以参考DSL中的课程。例:
package contracts.beer.rest
import com.example.ConsumerUtils
import com.example.ProducerUtils
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/check'
body(
age: $(ConsumerUtils.oldEnough())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status": "${value(ProducerUtils.ok())}"
}
""")
headers {
contentType(applicationJson())
}
}
}
可插拔架构
在某些情况下,您将合同定义为其他格式,如YAML,RAML或PACT。另一方面,您希望从测试和存根生成中获利。添加自己的任何一个实现是很容易的。此外,您还可以自定义测试生成的方式(例如,您可以为其他语言生成测试),并且可以对存根生成执行相同操作(可为其他存根http服务器实现生成存根)。
定制合同转换器
我们假设您的合同是用YAML文件写成的:
request:
url: /foo
method: PUT
headers:
foo: bar
body:
foo: bar
response:
status: 200
headers:
foo2: bar
body:
foo2: bar
感谢界面
package org.springframework.cloud.contract.spec
/**
* Converter to be used to convert FROM {@link File} TO {@link Contract}
* and from {@link Contract} to {@code T}
*
* @param <T> - type to which we want to convert the contract
*
* @author Marcin Grzejszczak
* @since 1.1.0
*/
interface ContractConverter<T> {
/**
* Should this file be accepted by the converter. Can use the file extension
* to check if the conversion is possible.
*
* @param file - file to be considered for conversion
* @return - {@code true} if the given implementation can convert the file
*/
boolean isAccepted(File file)
/**
* Converts the given {@link File} to its {@link Contract} representation
*
* @param file - file to convert
* @return - {@link Contract} representation of the file
*/
Collection<Contract> convertFrom(File file)
/**
* Converts the given {@link Contract} to a {@link T} representation
*
* @param contract - the parsed contract
* @return - {@link T} the type to which we do the conversion
*/
T convertTo(Collection<Contract> contract)
}
您可以注册自己的合同结构转换器的实现。您的实现需要说明开始转换的条件。此外,您必须定义如何以两种方式执行转换。
重要
|
创建实施后,您必须创建一个/META-INF/spring.factories 文件,您可以在其中提供实施的完全限定名称。
|
spring.factories
文件的示例
# Converters
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter
和YAML实现
package org.springframework.cloud.contract.verifier.converter
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.ContractConverter
import org.springframework.cloud.contract.spec.internal.Headers
import org.yaml.snakeyaml.Yaml
/**
* Simple converter from and to a {@link YamlContract} to a collection of {@link Contract}
*/
@CompileStatic
class YamlContractConverter implements ContractConverter<List<YamlContract>> {
@Override
public boolean isAccepted(File file) {
String name = file.getName()
return name.endsWith(".yml") || name.endsWith(".yaml")
}
@Override
public Collection<Contract> convertFrom(File file) {
try {
YamlContract yamlContract = new Yaml().loadAs(new FileInputStream(file), YamlContract.class)
return [Contract.make {
request {
method(yamlContract?.request?.method)
url(yamlContract?.request?.url)
headers {
yamlContract?.request?.headers?.each { String key, Object value ->
header(key, value)
}
}
body(yamlContract?.request?.body)
}
response {
status(yamlContract?.response?.status)
headers {
yamlContract?.response?.headers?.each { String key, Object value ->
header(key, value)
}
}
body(yamlContract?.response?.body)
}
}]
}
catch (FileNotFoundException e) {
throw new IllegalStateException(e)
}
}
@Override
public List<YamlContract> convertTo(Collection<Contract> contracts) {
return contracts.collect { Contract contract ->
YamlContract yamlContract = new YamlContract()
yamlContract.request.with {
method = contract?.request?.method?.clientValue
url = contract?.request?.url?.clientValue
headers = (contract?.request?.headers as Headers)?.asStubSideMap()
body = contract?.request?.body?.clientValue as Map
}
yamlContract.response.with {
status = contract?.response?.status?.clientValue as Integer
headers = (contract?.response?.headers as Headers)?.asStubSideMap()
body = contract?.response?.body?.clientValue as Map
}
return yamlContract
}
}
}
契约转换器
Spring Cloud Contract提供了协议代表合同的开箱即用的支持。换句话说,而不是使用Groovy DSL,您可以使用Pact文件。在本节中,我们将介绍如何为您的项目添加此类支持。
契约契约
我们将在下面的一个契约契约的例子中工作。我们将此文件放在src/test/resources/contracts
文件夹下。
{
"provider": {
"name": "Provider"
},
"consumer": {
"name": "Consumer"
},
"interactions": [
{
"description": "",
"request": {
"method": "PUT",
"path": "/fraudcheck",
"headers": {
"Content-Type": "application/vnd.fraud.v1+json"
},
"body": {
"clientId": "1234567890",
"loanAmount": 99999
},
"matchingRules": {
"$.body.clientId": {
"match": "regex",
"regex": "[0-9]{10}"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
},
"body": {
"fraudCheckStatus": "FRAUD",
"rejectionReason": "Amount too high"
},
"matchingRules": {
"$.body.fraudCheckStatus": {
"match": "regex",
"regex": "FRAUD"
}
}
}
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "2.4.18"
}
}
}
生产者契约
在生产者方面,您可以添加两个附加依赖关系的插件配置。一个是Spring Cloud Contract Pact支持,另一个表示您正在使用的当前Pact版本。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<version>2.4.18</version>
</dependency>
</dependencies>
</plugin>
当您执行应用程序的构建时,将会产生一个或多或少的这样的测试
@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/vnd.fraud.v1+json")
.body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("rejectionReason").isEqualTo("Amount too high");
// and:
assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
}
并且这样的存根看起来像这样
{
"uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"request" : {
"url" : "/fraudcheck",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"equalTo" : "application/vnd.fraud.v1+json"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.loanAmount == 99999)]"
}, {
"matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
"headers" : {
"Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
}
}
}
消费者契约
在生产者方面,您可以添加项目依赖关系两个附加依赖关系。一个是Spring Cloud Contract Pact支持,另一个表示您正在使用的当前Pact版本。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-pact</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-model</artifactId>
<version>2.4.18</version>
<scope>test</scope>
</dependency>
定制测试发生器
如果您想为Java生成不同语言的测试,或者您不满意我们为您构建Java测试的方式,那么您可以注册自己的实现来实现。
感谢界面
package org.springframework.cloud.contract.verifier.builder
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.file.ContractMetadata
/**
* Builds a single test.
*
* @since 1.1.0
*/
interface SingleTestGenerator {
/**
* Creates contents of a single test class in which all test scenarios from
* the contract metadata should be placed.
*
* @param properties - properties passed to the plugin
* @param listOfFiles - list of parsed contracts with additional metadata
* @param className - the name of the generated test class
* @param classPackage - the name of the package in which the test class should be stored
* @param includedDirectoryRelativePath - relative path to the included directory
* @return contents of a single test class
*/
String buildClass(ContractVerifierConfigProperties properties, Collection<ContractMetadata> listOfFiles,
String className, String classPackage, String includedDirectoryRelativePath)
/**
* Extension that should be appended to the generated test class. E.g. {@code .java} or {@code .php}
*
* @param properties - properties passed to the plugin
*/
String fileExtension(ContractVerifierConfigProperties properties)
}
您可以注册自己的生成测试的实现。再次提供一个合适的spring.factories
文件就足够了。例:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
com.example.MyGenerator
自定义存根发生器
如果要为WireMock生成其他存根服务器的存根,就可以插入您自己的此接口的实现:
package org.springframework.cloud.contract.verifier.converter
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.file.ContractMetadata
/**
* Converts contracts into their stub representation.
*
* @since 1.1.0
*/
@CompileStatic
interface StubGenerator {
/**
* Returns {@code true} if the converter can handle the file to convert it into a stub.
*/
boolean canHandleFileName(String fileName)
/**
* Returns the collection of converted contracts into stubs. One contract can
* result in multiple stubs.
*/
Map<Contract, String> convertContents(String rootName, ContractMetadata content)
/**
* Returns the name of the converted stub file. If you have multiple contracts
* in a single file then a prefix will be added to the generated file. If you
* provide the {@link Contract#name} field then that field will override the
* generated file name.
*
* Example: name of file with 2 contracts is {@code foo.groovy}, it will be
* converted by the implementation to {@code foo.json}. The recursive file
* converter will create two files {@code 0_foo.json} and {@code 1_foo.json}
*/
String generateOutputFileNameForInput(String inputFileName)
}
您可以注册自己的生成存根的实现。再次提供一个合适的spring.factories
文件就足够了。例:
# Stub converters
org.springframework.cloud.contract.verifier.converter.StubGenerator=\
org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter
默认实现是WireMock存根生成。
提示
|
您可以提供多个存根生成器实现。这样,例如从单个DSL作为输入,您可以例如生成WireMock存根和Pact文件! |
自定义Stub Runner
如果您决定使用自定义存根生成器,则还需要使用不同存根提供程序来运行存根的自定义方式。
让我们假设您正在使用Moco来构建您的存根。你写了一个正确的存根生成器,你的存根被放在一个JAR文件中。
为了Stub Runner知道如何运行存根,您必须定义一个自定义的HTTP Stub服务器实现。它可以看起来像这样:
package org.springframework.cloud.contract.stubrunner.provider.moco
import com.github.dreamhead.moco.bootstrap.arg.HttpArgs
import com.github.dreamhead.moco.runner.JsonRunner
import com.github.dreamhead.moco.runner.RunnerSetting
import groovy.util.logging.Slf4j
import org.springframework.cloud.contract.stubrunner.HttpServerStub
import org.springframework.util.SocketUtils
@Slf4j
class MocoHttpServerStub implements HttpServerStub {
private boolean started
private JsonRunner runner
private int port
@Override
int port() {
if (!isRunning()) {
return -1
}
return port
}
@Override
boolean isRunning() {
return started
}
@Override
HttpServerStub start() {
return start(SocketUtils.findAvailableTcpPort())
}
@Override
HttpServerStub start(int port) {
this.port = port
return this
}
@Override
HttpServerStub stop() {
if (!isRunning()) {
return this
}
this.runner.stop()
return this
}
@Override
HttpServerStub registerMappings(Collection<File> stubFiles) {
List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") }
.collect {
log.info("Trying to parse [{}]", it.name)
try {
return RunnerSetting.aRunnerSetting().withStream(it.newInputStream()).build()
} catch (Exception e) {
log.warn("Exception occurred while trying to parse file [{}]", it.name, e)
return null
}
}.findAll { it }
this.runner = JsonRunner.newJsonRunnerWithSetting(settings,
HttpArgs.httpArgs().withPort(this.port).build())
this.runner.run()
this.started = true
return this
}
@Override
boolean isAccepted(File file) {
return file.name.endsWith(".json")
}
}
并将其注册到您的spring.factories
文件中
org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
这样你就可以使用Moco来运行存根。
重要
|
如果您没有提供任何实现,那么将选择默认的 - 基于WireMock的。如果您提供多个,那么列表中的第一个将被选中。 |
自定义存根下载器
您可以自定义存根的下载方式。创建StubDownloaderBuilder
的实现就足够了
package com.example;
class CustomStubDownloaderBuilder implements StubDownloaderBuilder {
@Override
public StubDownloader build(final StubRunnerOptions stubRunnerOptions) {
return new StubDownloader() {
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration config) {
File unpackedStubs = retrieveStubs();
return new AbstractMap.SimpleEntry<>(
new StubConfiguration(config.getGroupId(), config.getArtifactId(), version,
config.getClassifier()), unpackedStubs);
}
File retrieveStubs() {
// here goes your custom logic to provide a folder where all the stubs reside
}
}
并将其注册到您的spring.factories
文件中
# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
com.example.CustomStubDownloaderBuilder
这样你就可以选择一个文件夹与你的存根的来源。
重要
|
如果您没有提供任何实现,那么默认选项将被选中。如果您提供repositoryRoot 属性或workOffline 标志,则将选择将从远程回购下载存根的Aether。如果您不提供这些值,则将选择将扫描类路径的ClasspathStubProvider 。如果您提供多个,则列表中的第一个将被选中。
|
链接
在这里,您可以找到有关Spring Cloud Contract验证器的有趣链接:
Spring Cloud Contract WireMock
重要
|
Spring Cloud发布Train BOM导入spring-cloud-contract-dependencies ,反过来又会排除WireMock所需的依赖关系。这可能导致一种情况,即使你不使用Spring Cloud Contract,那么你的依赖将会受到影响。
|
如果您有一个使用Tomcat作为嵌入式服务器的Spring Boot应用程序(例如(默认为spring-boot-starter-web
)),则可以将spring-cloud-contract-wiremock
添加到类路径中,然后添加@AutoConfigureWireMock
,以便可以在测试中使用Wiremock。Wiremock作为存根服务器运行,您可以使用Java API或通过静态JSON声明来注册存根行为,作为测试的一部分。这是一个简单的例子:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class WiremockForDocsTests {
// A service that calls out over HTTP
@Autowired private Service service;
// Using the WireMock APIs in the normal way:
@Test
public void contextLoads() throws Exception {
// Stubbing WireMock
stubFor(get(urlEqualTo("/resource"))
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
// We're asserting if WireMock responded properly
assertThat(this.service.go()).isEqualTo("Hello World!");
}
}
要使用不同的端口启动存根服务器,请使用@AutoConfigureWireMock(port=9999)
(例如),对于随机端口使用值0.存根服务器端口将在测试应用程序上下文中绑定为“wiremock.server.port”。使用@AutoConfigureWireMock
将一个类型为WiremockConfiguration
的bean添加到测试应用程序上下文中,它将被缓存在具有相同上下文的方法和类之间,就像普通的Spring集成测试一样。
自动注册存根
如果您使用@AutoConfigureWireMock
,则它将从文件系统或类路径注册WireMock JSON存根,默认情况下为file:src/test/resources/mappings
。您可以使用注释中的stubs
属性自定义位置,该属性可以是资源模式(ant-style)或目录,在这种情况下,附加*/.json
。例:
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureWireMock(存根= “类路径:/存根”) 公共类WiremockImportApplicationTests { @Autowired 私人服务; @测试 public void contextLoads()throws Exception { assertThat(this.service.go())。isEqualTo(“Hello World!”); } }
注意
|
实际上,WireMock总是从src/test/resources/mappings 加载映射以及 stubs属性中的自定义位置。要更改此行为,您还必须如下所述指定文件根。
|
使用文件指定存根体
WireMock可以从类路径或文件系统上的文件读取响应体。在这种情况下,您将在JSON DSL中看到响应具有“bodyFileName”而不是(文字)“body”。默认情况下,相对于根目录src/test/resources/__files
解析文件。要自定义此位置,您可以将@AutoConfigureWireMock
注释中的files
属性设置为父目录的位置(即,位置__files
是子目录)。您可以使用Spring资源符号来引用file:…
或classpath:…
位置(但不支持通用URL)。可以给出值列表,并且WireMock将在需要查找响应体时解析存在的第一个文件。
注意
|
当您配置files 根时,它会影响自动加载存根(它们来自称为“映射”的子目录中的根位置)。files 的值对从stubs 属性明确加载的存根没有影响。
|
替代方法:使用JUnit规则
对于更常规的WireMock体验,使用JUnit @Rules
启动和停止服务器,只需使用WireMockSpring
便利类来获取Options
实例:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class WiremockForDocsClassRuleTests {
// Start WireMock on some dynamic port
// for some reason `dynamicPort()` is not working properly
@ClassRule
public static WireMockClassRule wiremock = new WireMockClassRule(
WireMockSpring.options().dynamicPort());
// A service that calls out over HTTP to localhost:${wiremock.port}
@Autowired
private Service service;
// Using the WireMock APIs in the normal way:
@Test
public void contextLoads() throws Exception {
// Stubbing WireMock
wiremock.stubFor(get(urlEqualTo("/resource"))
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
// We're asserting if WireMock responded properly
assertThat(this.service.go()).isEqualTo("Hello World!");
}
}
使用@ClassRule
表示服务器将在此类中的所有方法之后关闭。
休息模板的轻松SSL验证
WireMock允许您使用“https”URL协议存根“安全”服务器。如果您的应用程序想在集成测试中联系该存根服务器,则会发现SSL证书无效(这是自行安装证书的常见问题)。通常最好的选择是重新配置客户端使用“http”,但是如果这样不开放,那么您可以要求Spring配置一个忽略SSL验证错误的HTTP客户端(仅用于测试)。
为了使此功能最小化,您需要在应用程序中使用Spring Boot RestTemplateBuilder
,例如
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
这是因为构建器通过回调来初始化它,因此可以在客户端中设置SSL验证。如果您使用@AutoConfigureWireMock
注释(或存根转移),测试中会自动发生。如果您使用JUnit @Rule
方法,您还需要添加@AutoConfigureHttpClient
注释:
@RunWith(SpringRunner.class)
@SpringBootTest("app.baseUrl=https://localhost:6443")
@AutoConfigureHttpClient
public class WiremockHttpsServerApplicationTests {
@ClassRule
public static WireMockClassRule wiremock = new WireMockClassRule(
WireMockSpring.options().httpsPort(6443));
...
}
如果您使用spring-boot-starter-test
,则您将在类路径上具有Apache HTTP客户端,并且将由RestTemplateBuilder
选择并配置为忽略SSL错误。如果您使用默认的java.net
客户端,则不需要注释(但不会造成任何伤害)。目前没有其他客户端的支持,但可能会在将来的版本中添加。
WireMock和Spring MVC模拟器
Spring Cloud Contract提供了一个方便的类,可以将JSON WireMock存根加载到Spring MockRestServiceServer
中。以下是一个例子:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class WiremockForDocsMockServerApplicationTests {
@Autowired
private RestTemplate restTemplate;
@Autowired
private Service service;
@Test
public void contextLoads() throws Exception {
// will read stubs classpath
MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
.baseUrl("http://example.org").stubs("classpath:/stubs/resource.json")
.build();
// We're asserting if WireMock responded properly
assertThat(this.service.go()).isEqualTo("Hello World");
server.verify();
}
}
baseUrl
前面提到所有的模拟调用,stubs()
方法采用stub路径资源模式作为参数。所以在这个例子中,定义在/stubs/resource.json
的存根被加载到模拟服务器中,所以如果RestTemplate
被要求访问http://example.org/
,那么它将得到声明的响应。可以指定多个存根模式,每个可以是一个目录(对于所有“.json”的递归列表)或一个固定的文件名(如上例所示)或一个蚂蚁样式模式。JSON格式是通常的WireMock格式,您可以在WireMock网站上阅读。
目前,我们支持Tomcat,Jetty和Undertow作为Spring Boot嵌入式服务器,而Wiremock本身对特定版本的Jetty(目前为9.2)具有“本机”支持。要使用本地Jetty,您需要添加本机的线索依赖关系,并排除Spring Boot容器(如果有)。
使用RestDocs生成存根
Spring RestDocs可以用于为具有Spring MockMvc或Rest Assured的HTTP API生成文档(例如,asciidoctor格式)。在生成API文档的同时,还可以使用Spring Cloud Contract WireMock生成WireMock存根。只需编写正常的RestDocs测试用例,并使用@AutoConfigureRestDocs
在restdocs输出目录中自动存储存根。例如:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void contextLoads() throws Exception {
mockMvc.perform(get("/resource"))
.andExpect(content().string("Hello World"))
.andDo(document("resource"));
}
}
从此测试将在“target / snippets / stubs / resource.json”上生成一个WireMock存根。它将所有GET请求与“/ resource”路径相匹配。
没有任何其他配置,这将创建一个存根与HTTP方法的请求匹配器和除“主机”和“内容长度”之外的所有头。为了更准确地匹配请求,例如要匹配POST或PUT的正文,我们需要明确创建一个请求匹配器。这将做两件事情:1)创建一个只匹配您指定的方式的存根,2)断言测试用例中的请求也匹配相同的条件。
主要的入口点是WireMockRestDocs.verify()
,可以替代document()
便利方法。例如:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
.andExpect(status().isOk())
.andDo(verify().jsonPath("$.id")
.stub("resource"));
}
}
所以这个合同是说:任何带有“id”字段的有效POST都会得到与本测试相同的响应。您可以将来电链接到.jsonPath()
以添加其他匹配器。该
JayWay文档可以帮助你起床的速度与JSON路径,如果它是你不熟悉的。
您也可以使用WireMock API来验证请求与创建的存根是否匹配,而不是jsonPath
和contentType
方便的方法。例:
@Test
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
.andExpect(status().isOk())
.andDo(verify()
.wiremock(WireMock.post(
urlPathEquals("/resource"))
.withRequestBody(matchingJsonPath("$.id"))
.stub("post-resource"));
}
WireMock API是丰富的 - 您可以通过正则表达式以及json路径来匹配头文件,查询参数和请求正文,因此这可以用于创建具有更广泛参数的存根。上面的例子会生成一个这样的stub:
{
"request" : {
"url" : "/resource",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$.id"
}]
},
"response" : {
"status" : 200,
"body" : "Hello World",
"headers" : {
"X-Application-Context" : "application:-1",
"Content-Type" : "text/plain"
}
}
}
注意
|
您可以使用wiremock() 方法或jsonPath() 和contentType() 方法来创建请求匹配器,但不能同时使用两者。
|
使用RestDocs生成Contracts
可以使用Spring RestDocs生成的另一件事是Spring Cloud Contract DSL文件和文档。如果您将这与Spring Cloud WireMock相结合,那么您将收到合同和存根。
为什么要使用此功能?有些社区人士询问有关他们想转到基于DSL的合同定义的情况,但他们已经有很多Spring MVC测试。使用此功能,您可以生成可以稍后修改的合同文件,并移动到正确的文件夹,以便插件选择它们。
提示
|
您可能会想知道为什么该功能在WireMock模块中。来想一想,它确实有道理,因为只生成合同并且不生成存根就没有意义。这就是为什么我们建议做这两个。 |
我们来想象下面的测试:
this.mockMvc.perform(post("/foo")
.accept(MediaType.APPLICATION_PDF)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"foo\": 23 }"))
.andExpect(status().isOk())
.andExpect(content().string("bar"))
// first WireMock
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.foo >= 20)]")
.contentType(MediaType.valueOf("application/json"))
.stub("shouldGrantABeerIfOldEnough"))
// then Contract DSL documentation
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
这将导致在上一节中介绍的存根的创建,合同将被生成和文档文件。
合同将被称为index.groovy
,看起来更像是这样。
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method 'POST'
url '/foo'
body('''
{"foo": 23 }
''')
headers {
header('''Accept''', '''application/json''')
header('''Content-Type''', '''application/json''')
}
}
response {
status 200
body('''
bar
''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
header('''Content-Length''', '''3''')
}
testMatchers {
jsonPath('$[?(@.foo >= 20)]', byType())
}
}
}
生成的文档(Asciidoc的示例)将包含格式化的合同(此文件的位置为index/dsl-contract.adoc
)。