Spring Cloud Contract验证者
介绍
重要 | 1.1.0版中已弃用的Accurest项目的文档可在此处获取。 |
提示 | 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移动到软件体系结构的层次。
Spring Cloud Contract视频
您可以查看华沙JUG关于Spring Cloud Contract的视频: 点击此处查看视频
为什么?
让我们假设我们有一个由多个微服务组成的系统:
测试问题
如果我们想测试应用程序在左上角,如果它可以与其他服务通信,那么我们可以做两件事之一:
- 部署所有微服务器并执行端到端测试
- 模拟其他微型服务单元/集成测试
两者都有其优点,但也有很多缺点。我们来关注后者。
部署所有微服务器并执行端到端测试
优点:
- 模拟生产
- 测试服务之间的真实沟通
缺点:
- 要测试一个微服务器,我们将不得不部署6个微服务器,几个数据库等。
- 将进行测试的环境将被锁定用于一套测试(即没有人能够在此期间运行测试)。
- 长跑
- 非常迟的反馈
- 非常难调试
模拟其他微型服务单元/集成测试
优点:
- 非常快的反馈
- 没有基础架构要求
缺点:
- 服务的实现者创建存根,因此它们可能与现实无关
- 您可以通过测试和生产不合格进行生产
为了解决上述问题,Spring Cloud Contract Stub Runner的验证器被创建。他们的主要思想是给你非常快的反馈,而不需要建立整个微服务的世界。
如果您在存根上工作,那么您需要的唯一应用是应用程序直接使用的应用程序。
Spring Cloud Contract验证者确定您使用的存根是由您正在调用的服务创建的。此外,如果您可以使用它们,这意味着它们是针对生产者的一方进行测试的。换句话说 - 你可以信任这些存根。
目的
Spring Cloud Contract验证器与Stub Runner的主要目的是:
- 确保WireMock / Messaging存根(在开发客户端时使用)正在完全实际执行服务器端实现,
- 推广ATDD方法和微服务架构风格,
- 提供一种发布双方立即可见的合同变更的方法,
- 生成服务器端使用的样板测试代码。
重要 | Spring Cloud Contract验证者的目的不是开始在合同中编写业务功能。我们假设我们有一个欺诈检查的商业用例。如果一个用户因为100个不同的原因而被欺骗,我们假设你会创建2个合同。一个为正面,一个为负面欺诈案。合同测试用于测试应用程序之间的合同,而不是模拟完整行为。 |
客户端
在测试期间,您希望启动并运行一个模拟服务Y的WireMock实例/消息传递路由。您希望为该实例提供适当的存根定义。该存根定义将需要有效,并且也应在服务器端可重用。
总结:在这方面,在存根定义中,您可以使用模式进行请求存根,并需要确切的响应值。
服务器端
作为开发您的存根的服务Y,您需要确保它实际上类似于您的具体实现。您不能以某种方式存在您的存根行为,并且您的生产应用程序以不同的方式运行。
这就是为什么会生成提供的存根验收测试,这将确保您的应用程序的行为与您在存根中定义的相同。
总结:在这方面,在存根定义中,您需要精确的值作为请求,并可以使用模式/方法进行响应验证。
逐步向CDC指导
举一个欺诈检测和贷款发行流程的例子。业务情景是这样的,我们想向人们发放贷款,但不希望他们从我们那里偷钱。目前我们的系统实施给大家贷款。
假设Loan Issuance
是Fraud Detection
服务器的客户端。在目前的冲刺中,我们需要开发一个新的功能 - 如果客户想要借到太多的钱,那么我们将他们标记为欺诈。
技术说明 - 欺诈检测将具有工件ID http-server
,贷款发行http-client
,两者都具有组ID com.example
。
社会声明 - 客户端和服务器开发团队都需要直接沟通,并在整个过程中讨论变更。CDC是关于沟通的。
提示 | 在这种情况下,合同的所有权在生产者方面。这意味着物理上所有的合同都存在于生产者存储库中 |
技术说明
如果使用SNAPSHOT / 里程碑 / 版本候选版本,请将以下部分添加到您的
Maven的<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>
摇篮repositories {
mavenCentral()
mavenLocal()
maven { url "http://repo.spring.io/snapshot" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/release" }
}
消费方(贷款发行)
作为贷款发行服务(欺诈检测服务器的消费者)的开发人员:
通过对您的功能进行测试开始做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
网址。
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/contract/fraud 下。fraud 文件夹是重要的,因为我们将在生产者的测试基类中引用该文件夹。 |
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
clientId: $(regex('[0-9]{10}')),
loanAmount: 99999
])
headers { // (5)
contentType('application/vnd.fraud.v1+json')
}
}
response { // (6)
status 200 // (7)
body([ // (8)
fraudCheckStatus: "FRAUD",
rejectionReason: "Amount too high"
])
headers { // (9)
contentType('application/vnd.fraud.v1+json')
}
}
}
/*
Since we don't want to force on the user to hardcode values of fields that are dynamic
(timestamps, database ids etc.), one can parametrize those entries. If you wrap your field's
value in a `$(...)` or `value(...)` and provide a dynamic value of a field then
the concrete value will be generated for you. If you want to be really explicit about
which side gets which value you can do that by using the `value(consumer(...), producer(...))` notation.
That way what's present in the `consumer` section will end up in the produced stub. What's
there in the `producer` will end up in the autogenerated test. If you provide only the
regular expression side without the concrete value then Spring Cloud Contract will generate one for you.
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/vnd.fraud.v1+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/vnd.fraud.v1+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/vnd.fraud.v1+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/vnd.fraud.v1+json.*`
*/
合同使用静态类型的Groovy DSL编写。你可能想知道这些value(client(…), server(…))
部分是什么。通过使用这种符号Spring Cloud Contract,您可以定义动态的JSON / URL /等的部分。在标识符或时间戳的情况下,您不想硬编码一个值。你想允许一些不同的值范围。这就是为什么对于消费者端,您可以设置与这些值匹配的正则表达式。您可以通过地图符号或带插值的String来提供身体。 有关更多信息,请参阅文档。我们强烈推荐使用地图符号!
提示 | 了解地图符号设置合同非常重要。请阅读有关JSON的 Groovy文档 |
上述合同是双方达成的协议:
- 如果发送了HTTP请求
- 端点
/fraudcheck
上的方法PUT
- 与
clientPesel
的正则表达式[0-9]{10}
和loanAmount
等于99999
的JSON体 - 并且标题
Content-Type
等于application/vnd.fraud.v1+json
- 端点
- 那么HTTP响应将被发送给消费者
- 状态为
200
- 包含JSON体,其中包含值为
FRAUD
的fraudCheckStatus
字段和值为Amount too high
的rejectionReason
字段 - 和一个值为
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.0.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,
consumes = FRAUD_SERVICE_JSON_VERSION_1,
produces = FRAUD_SERVICE_JSON_VERSION_1)
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/contract/fraud 。由于我们没有从contracts 文件夹开始有2个包,我们只挑选一个是fraud 。我们正在添加Base 后缀,我们正在大写fraud 。这给了我们FraudBase 测试类名称。 |
这是因为所有生成的测试都会扩展该类。在那里你可以设置你的Spring上下文或任何必要的。在我们的例子中,我们使用Rest Assured MVC来启动服务器端FraudDetectionController
。
package com.example.fraud;
import org.junit.Before;
import com.jayway.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("{\"clientPesel\":\"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("rejectionReason").isEqualTo("Amount too high");
}
您可以看到value(consumer(…), producer(…))
块中存在的所有producer()
部分合同注入测试。
重要的是在生产者方面,我们也在做TDD。我们有一个测试形式的期望。此测试正在向我们自己的应用程序拍摄一个在合同中定义的URL,标题和主体的请求。它也期待响应中非常精确地定义的值。换句话说,您是red
green
和refactor
的red
部分。将red
转换为green
的时间。
写入缺少的实现
现在,由于我们现在预期的输入和预期的输出是什么,我们来写这个缺少的实现。
@RequestMapping(
value = "/fraudcheck",
method = PUT,
consumes = FRAUD_SERVICE_JSON_VERSION_1,
produces = FRAUD_SERVICE_JSON_VERSION_1)
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验证程序项目正在不断发展。
阅读
- 来自Marcin Grzejszczak关于Accurest的演讲
- 来自Marcin Grzejszczak的博客的Accurest相关文章
- 来自Marcin Grzejszczak博客的Spring Cloud Contract相关文章
- Groovy关于JSON的文档
样品
在这里可以找到一些样品。
常问问题
为什么使用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"
])
}
}
重要 | 请阅读与JSON相关的Groovy文档,以了解如何正确构建请求/响应实体。 |
如何做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.0.BUILD-SNAPSHOT</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<spring-cloud-contract.version>1.1.0.BUILD-SNAPSHOT</spring-cloud-contract.version>
<spring-cloud-dependencies.version>Dalston.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和依赖关系:
<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
下的合同作为用于生成测试和存根的选择。由于这个惯例,生产者团队将会知道当一些不兼容的更改完成时,哪些消费者团队将被破坏。
其余的流程看起来是一样的。
我可以有多个基类进行测试吗?
是! 查看Gradle或Maven插件的合同部分的不同基类。
如何调试生成的测试客户端发送的请求/响应?
生成的测试都以某种形式或时尚的方式依赖于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'
}
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
}
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
}
}
}
配置插件
要更改默认配置,只需在您的Gradle配置中添加contracts
代码段即可
contracts {
testMode = 'MockMvc'
baseClassForTests = 'org.mycompany.tests'
generatedTestSourcesDir = project.file('src/generatedContract')
}
配置选项
- testMode - 定义接受测试的模式。默认的基于Spring的MockMvc的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
关闭来设置它 - contractPath - 如果下载合同部分将默认为
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的快照版本
对于快照/里程碑版本,您必须将以下部分添加到您的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
。 - contractDir - 包含使用GroovyDSL编写的合同的目录。默认
/src/test/resources/contracts
。 - testFramework - 要使用的目标测试框架; JUnit作为默认框架,目前支持Spock和JUnit
- packageWithBaseClasses - 而不是为基类提供固定值,您可以提供一个所有基类放置的包。约定是这样的,如果你有合同
src/test/resources/contract/foo/bar/baz/
,并提供这个属性的值到com.example.base
,那么我们将假设com.example.base
包含com.example.base
类。优先于baseClassForTests - baseClassMappings - 您必须提供
contractPackageRegex
的基类映射列表,该列表根据合同所在的包进行检查,并且baseClassFQN
映射到匹配合同的基类的完全限定名称。如果您有合同src/test/resources/contract/foo/bar/baz/
并映射了属性.*
→com.example.base.BaseClass
,则从这些合同生成的测试类将扩展com.example.base.BaseClass
。优先于packageWithBaseClasses 和baseClassForTests。
如果要从Maven存储库中下载合同定义,可以使用
- contractsRepositoryUrl - 具有合同的工件的repo的URL(如果没有提供)应使用当前的Maven
- contractDependency - 包含所有打包合同的合同依赖关系
- contractPath - 通过打包合同在JAR中具体合同的路径。默认为
groupid/artifactid
,其中gropuid
被斜杠分隔。 - contractWorkOffline - 如果依赖关系应该被下载,或者本地Maven只能被重用
有关完整信息,请参阅插件文档
所有测试的单一基类
在默认的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>
制图
您可以手动将合同包的正则表达式映射为匹配合同的基类的完全限定名称。您必须提供baseClassMappings
baseClassMapping
的contractPackageRegex
列表contractPackageRegex
到baseClassFQN
映射。我们来看看下面的例子:
<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,但您也可以自己创建并使用它。
集成
您可以使用四种集成配置之一:
- Apache Camel
- Spring Integration
- Spring Cloud Stream
- Spring AMQP
由于我们使用Spring Boot,因此如果您已经将上述的一个库添加到类路径中,那么将自动设置所有的消息传递配置。
重要 | 记住将@AutoConfigureMessageVerifier 放在生成的测试的基类上。否则Spring Cloud Contract验证器的消息传递部分将无法正常工作。 |
手动集成测试
测试使用的主界面是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:输入消息被消耗,没有输出消息
情景1(无输入讯息)
对于给定的合同:
def contractDsl = Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
将创建以下JUnit测试:
'''
// when:
bookReturnedTriggered();
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
assertThat(response).isNotNull();
assertThat(response.getHeader("BOOK-NAME")).isEqualTo("foo");
// 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') == 'foo'
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")).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') == '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消息部分。
Maven的<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>
摇篮ext {
contractsDir = file("mappings")
stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
}
// Automatically added by plugin:
// copyContracts - copies contracts to the output folder from which JAR will be created
// verifierStubsJar - JAR with a provided stub suffix
// the presented publication is also added by the plugin but you can modify it as you wish
publishing {
publications {
stubs(MavenPublication) {
artifactId "${project.name}-stubs"
artifact verifierStubsJar
}
}
}
Spring Cloud Contract Stub Runner
使用Spring Cloud Contract验证程序时可能遇到的一个问题是将生成的WireMock JSON存根从服务器端传递到客户端(或各种客户端)。在消息传递的客户端生成方面也是如此。
复制JSON文件/手动设置客户端进行消息传递是不成问题的。
这就是为什么我们会介绍可以为您自动下载和运行存根的Spring Cloud Contract Stub Runner。
快照版本
将其他快照存储库添加到您的build.gradle以使用快照版本,每次成功构建后都会自动上传:
Maven的<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>
摇篮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" }
}
将存根发布为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 -->
<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>$/Users/sgibb/workspace/spring/spring-cloud-samples/scripts/docs/../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>$/Users/sgibb/workspace/spring/spring-cloud-samples/scripts/docs/../src/test/resources/contracts</directory>
<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory>
<includes>
<include>**/*.groovy</include>
</includes>
</fileSet>
</fileSets>
</assembly>
摇篮ext {
contractsDir = file("mappings")
stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
}
// Automatically added by plugin:
// copyContracts - copies contracts to the output folder from which JAR will be created
// verifierStubsJar - JAR with a provided stub suffix
// the presented publication is also added by the plugin but you can modify it as you wish
publishing {
publications {
stubs(MavenPublication) {
artifactId "${project.name}-stubs"
artifact verifierStubsJar
}
}
}
模块
Stub Runner核心
为服务合作者运行存根。作为服务合同处理存根允许使用stub-runner作为 Consumer Driven Contracts的实现。
Stub Runner允许您自动下载提供的依赖项的存根,为其启动WireMock服务器,并为其提供适当的存根定义。对于消息传递,定义了特殊的存根路由。
运行存根
限制
重要 | 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文件
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的公共属性。
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与应用程序的名称进行匹配。提供:stubrunner.cloud.ribbon.enabled
等于false
,您可以禁用Stub Runner Ribbon支持。您可以通过提供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服务器。默认情况下,application.yml
可能会设置1-3,因为它们不太可能改变。这样,只要您启动Stub Runner引导,您只能提供要下载的存根列表。
JUnit和Spring的常用属性
可以使用系统属性或配置属性(对于Spring)设置重复的某些属性。以下是他们的名称及其默认值:
物业名称 | 默认值 | 描述 |
---|---|---|
stubrunner.minPort | 10000 | 具有存根的起始WireMock端口的最小值 |
stubrunner.maxPort | 15000 | 具有存根的起始WireMock端口的最小值 |
stubrunner.repositoryRoot | Maven repo网址 如果空白,那么将调用本地的maven repo | |
stubrunner.classifier | stubs | stub工件的默认分类器 |
stubrunner.workOffline | false | 如果为true,则不会联系任何远程存储库以下载存根 |
stubrunner.ids | 数组的常春藤符号存根下载 | |
stubrunner.username | 可选的用户名访问使用存根存储JAR的工具 | |
stubrunner.password | 访问使用存根存储JAR的工具的可选密码 |
存根运动员短桩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
- Apache Camel
- 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就足够了。记住使用@AutoConfigureMessageVerifier
注释你的测试类。
例子
桩结构
让我们假设我们拥有以下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集成的方法。对于提供的工件,它将自动下载存根并注册所需的路由。
将其添加到项目中
在类路径上同时拥有Apache Camel和Spring Cloud Contract Stub Runner就足够了。记住使用@AutoConfigureMessageVerifier
注释测试类。
例子
桩结构
让我们假设我们拥有以下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流
Spring Cloud Contract验证器Stub Runner的消息传递模块为您提供了与Spring Stream集成的简单方式。对于提供的工件,它将自动下载存根并注册所需的路由。
警告 | 在Stub Runner与Stream的集成中,messageFrom 或sentTo 字符串首先被解析为一个destination 的频道,然后如果没有这样的destination ,它被解析为频道名称。 |
将其添加到项目中
在类路径上同时拥有Apache Camel和Spring Cloud Contract Stub Runner就足够了。记住用@AutoConfigureMessageVerifier
注释你的测试类。
例子
桩结构
让我们假设我们拥有以下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
。记住用@AutoConfigureMessageVerifier
注释你的测试类。
例子
桩结构
让我们假设我们拥有以下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;
}
注意 | 该消息直接交给MessageListener 与SimpleMessageListenerContainer 匹配的MessageListener 方法。 |
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 Map符号。 |
常见的顶级元素
描述
您可以添加一个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 {
//...
}
}
响应
最小响应必须包含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 = Pattern.compile('((www\\.|(http|https|ftp|news|file)+\\:\\/\\/)[_.a-z0-9-]+\\.[a-z0-9\\/_:@=.+?,##%&~-]*[^.|\\\'|\\# |!|\\(|?|,| |>|<|;|\\)])')
protected static final Pattern UUID = Pattern.compile('[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-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() 方法返回您需要的所有内容。 |
从响应引用请求
最好的情况是提供固定值,但有时您需要在响应中引用请求。为了做到这一点,您可以从fromRequest()
方法中获利,从而允许您从HTTP请求中引用一堆元素。您可以使用以下选项:
fromRequest().url()
- 返回请求URLfromRequest().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
方法。
我们来看看下面的例子:
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"
])
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())
}
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: [],
])
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)'))
}
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" : "GET",
"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模式。
Maven的<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>
摇篮contracts {
testMode = 'EXPLICIT'
}
这样就可以生成不使用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
*/
public class PatternUtils {
public static String tooYoung() {
return "[0-1][0-9]";
}
public static Pattern oldEnough() {
return Pattern.compile("[2-9][0-9]");
}
public static Pattern anyName() {
return Pattern.compile("[a-zA-Z]+");
}
/**
* Makes little sense but it's just an example ;)
*/
public static Pattern ok() {
return Pattern.compile("OK");
}
}
ConsumerUtils包含由使用功能的消费者。
package com.example;
import org.springframework.cloud.contract.spec.internal.ClientDslProperty;
import org.springframework.cloud.contract.spec.internal.DslProperty;
/**
* 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
*/
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 the consumer side value of age field will be
* a regular expression and the producer side will be generated.
*
* @author Marcin Grzejszczak
*/
public static ClientDslProperty oldEnough() {
return new ClientDslProperty(PatternUtils.oldEnough());
}
/**
* 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(
* [ name: $(ConsumerUtils.anyName())]
* )
* }
* </pre>
*
* That way the consumer will be a regular expression and the
* producer side value will be equal to {@code marcin}
*/
public static DslProperty anyName() {
return new DslProperty<>(PatternUtils.anyName(), "marcin");
}
}
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
*/
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 the producer side value of age field will be
* a regular expression and the consumer side will be generated.
*/
public static ServerDslProperty ok() {
return new ServerDslProperty(PatternUtils.ok());
}
}
将依赖项添加到项目中
为了使插件和IDE能够引用常见的JAR类,您需要将依赖关系传递给您的项目。
测试依赖项目的依赖关系
首先将常见的jar依赖项添加为测试依赖关系。这样,由于您的合同文件在测试资源路径中可用,所以公用的jar类将自动显示在您的Groovy文件中。
Maven的<dependency>
<groupId>com.example</groupId>
<artifactId>beer-common</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
摇篮testCompile("com.example:beer-common:0.0.1-SNAPSHOT")
测试插件依赖关系
现在你必须添加插件的依赖关系,以便在运行时重用。
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</packageWithBaseClasses>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>beer-common</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</plugin>
摇篮classpath "com.example:beer-common:0.0.1-SNAPSHOT"
在DSL中引用类
现在您可以参考DSL中的课程。例:
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
import static com.example.ConsumerUtils.oldEnough
import static com.example.ProducerUtils.ok
Contract.make {
request {
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
""")
method 'POST'
url '/check'
body(
age: $(oldEnough())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status": "${value(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版本。
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>
<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>
摇篮classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}"
classpath 'au.com.dius:pact-jvm-model:2.4.18'
当您执行应用程序的构建时,将会产生一个或多或少的这样的测试
@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版本。
Maven的<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>
摇篮testCompile "org.springframework.cloud:spring-cloud-contract-spec-pact"
testCompile 'au.com.dius:pact-jvm-model:2.4.18'
定制测试发生器
如果您想为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 org.springframework.cloud.contract.stubrunner.HttpServerStub
import org.springframework.util.SocketUtils
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<InputStream> streams = stubFiles.collect { it.newInputStream() }
this.runner = JsonRunner.newJsonRunnerWithStreams(streams,
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
文件中
# Example of a custom HTTP Server Stub
org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
这样你就可以使用Moco来运行存根。
重要 | 如果您不提供任何实现,那么将选择默认的 - 基于WireMock的。如果您提供多个,那么列表中的第一个将被选中。 |
自定义存根下载器
您可以自定义存根的下载方式。如果您不想以默认方式从Nexus / Artifactory下载JAR,您可以设置自己的实现。下面您可以找到一个Stub Downloader Provider示例,它从classpath的测试资源获取json
文件,将它们复制到临时文件,然后将该临时文件夹作为存根的根传递。
package org.springframework.cloud.contract.stubrunner.provider.moco
import org.springframework.cloud.contract.stubrunner.StubConfiguration
import org.springframework.cloud.contract.stubrunner.StubDownloader
import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder
import org.springframework.cloud.contract.stubrunner.StubRunnerOptions
import org.springframework.core.io.DefaultResourceLoader
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import java.nio.file.Files
/**
* Poor man's version of taking stubs from classpath. It needs much more
* love and attention to go to the main sources.
*
* @author Marcin Grzejszczak
*/
class ClasspathStubProvider implements StubDownloaderBuilder {
private static final int TEMP_DIR_ATTEMPTS = 10000
@Override
public StubDownloader build(StubRunnerOptions stubRunnerOptions) {
final StubConfiguration configuration = stubRunnerOptions.getDependencies().first()
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
new DefaultResourceLoader())
try {
String rootFolder = repoRoot(stubRunnerOptions) ?: "**/" + separatedArtifact(configuration) + "/**/*.json"
Resource[] resources = resolver.getResources(rootFolder)
final File tmp = createTempDir()
tmp.deleteOnExit()
// you'd have to write an impl to maintain the folder structure
// this is just for demo
resources.each { Resource resource ->
Files.copy(resource.getInputStream(), new File(tmp, resource.getFile().getName()).toPath())
}
return new StubDownloader() {
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration stubConfiguration) {
return new AbstractMap.SimpleEntry(configuration, tmp)
}
}
} catch (IOException e) {
throw new IllegalStateException(e)
}
}
private String repoRoot(StubRunnerOptions stubRunnerOptions) {
switch (stubRunnerOptions.stubRepositoryRoot) {
case { !it }:
return ""
case { String root -> root.endsWith("**/*.json") }:
return stubRunnerOptions.stubRepositoryRoot
default:
return stubRunnerOptions.stubRepositoryRoot + "/**/*.json"
}
}
private String separatedArtifact(StubConfiguration configuration) {
return configuration.getGroupId().replace(".", File.separator) +
File.separator + configuration.getArtifactId()
}
// Taken from Guava
private File createTempDir() {
File baseDir = new File(System.getProperty("java.io.tmpdir"))
String baseName = System.currentTimeMillis() + "-"
for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
File tempDir = new File(baseDir, baseName + counter)
if (tempDir.mkdir()) {
return tempDir
}
}
throw new IllegalStateException(
"Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName + (
TEMP_DIR_ATTEMPTS - 1) + ")")
}
}
并将其注册到您的spring.factories
文件中
# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
org.springframework.cloud.contract.stubrunner.provider.moco.ClasspathStubProvider
这样你就可以选择一个文件夹与你的存根的来源。
重要 | 如果您没有提供任何实现,那么将选择从远程备份中下载存根的默认Aether。如果您提供多个,那么列表中的第一个将被选中。 |
链接
在这里,您可以找到有关Spring Cloud Contract验证器的有趣链接: