返回

Spring Cloud 合约功能

发布时间:2022-12-07 03:40:50 295
# java# spring# kotlin# 数据# 服务器

Spring Cloud 合约功能_元数据

本节将深入探讨春云合约的详细信息。在这里你可以了解密钥 您可能想要使用和自定义的功能。如果您尚未这样做,则 可能想阅读“入门.html​”和 “使用.html”部分,以便您在 基本。

1. 合同DSL

Spring Cloud 合约支持使用以下语言编写的 DSL:

  • 槽的
  • 亚姆
  • 爪哇岛
  • 科特林

Spring 云合约支持在单个文件中定义多个合约。

以下示例显示了一个协定定义:

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": "https://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
}
}]
'''
}
response {
status OK()
}
}

 

您可以使用以下独立的 Maven 命令编译协定到存根的映射:

mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert

 

​​1.1. Groovy 中的合约 DSL​​

如果您不熟悉Groovy,请不要担心。您可以在 时髦的DSL文件也是如此。

如果您决定用Groovy编写合约,如果您没有使用Groovy,请不要惊慌。 以前。该语言的知识并不是真正需要的,因为契约DSL只使用一个 它的一小部分(仅文字、方法调用和闭包)。此外,DSL 是静态的 类型,使其程序员可读,而无需了解DSL本身。

 

请记住,在Groovy合约文件中,您必须提供完整的 类和静态导入的限定名称,例如。您还可以提供导入到 类 (),然后调用。​​Contract​​​​make​​​​org.springframework.cloud.spec.Contract.make { … }​​​​Contract​​​​import org.springframework.cloud.spec.Contract​​​​Contract.make { … }​

1.2. Java中的契约DSL

要用 Java 编写合约定义,您需要创建一个实现接口(针对单个合约)或(针对多个合约)的类。​​Supplier​​​​Supplier<Collection>​

您还可以在(例如)下编写协定定义,这样就不必修改项目的类路径。在这种情况下,您必须为Spring Cloud合约插件提供合约定义的新位置。​​src/test/java​​​​src/test/java/contracts​

以下示例(在 Maven 和 Gradle 中)具有以下协定定义:​​src/test/java​


org.springframework.cloud
spring-cloud-contract-maven-plugin
${spring-cloud-contract.version}
true

src/test/java/contracts

1.3. Kotlin 中的合约 DSL

要开始使用 Kotlin 编写合约,您需要从一个(新创建的)Kotlin 脚本文件 () 开始。 与Java DSL一样,您可以将合约放在您选择的任何目录中。 默认情况下,Maven 插件将查看目录,而 Gradle 插件将 查看目录。​​.kts​​​​src/test/resources/contracts​​​​src/contractTest/resources/contracts​

从 3.0.0 开始,Gradle 插件也将查看旧版 目录用于迁移目的。在此目录中找到合约时,会显示警告 将在构建期间记录。​​src/test/resources/contracts​

您需要将依赖项显式传递给项目插件设置。 以下示例(在 Maven 和 Gradle 中)展示了如何执行此操作:​​spring-cloud-contract-spec-kotlin​

<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- some config -->
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-kotlin</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>

<dependencies>
<!-- Remember to add this for the DSL support in the IDE and on the consumer side -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-kotlin</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

请记住,在 Kotlin 脚本文件中,您必须为类提供完全限定的名称。 通常,您将按如下方式使用其合约函数: 您还可以提供对函数 () 的导入,然后调用。​​ContractDSL​​​​org.springframework.cloud.contract.spec.ContractDsl.contract { … }​​​​contract​​​​import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract​​​​contract { … }​

1.4. YAML 中的合约 DSL

要查看 YAML 合约的架构,请访问YML 架构页面。

1.5. 限制

对验证 JSON 数组大小的支持是实验性的。如果需要帮助, 若要将其打开,请将以下系统属性的值设置为:“。默认情况下,此功能设置为 。 您还可以在插件配置中设置属性。​​true​​​​spring.cloud.contract.verifier.assert.size​​​​false​​​​assertJsonSize​

由于 JSON 结构可以具有任何形式,因此无法解析它 正确使用时髦的DSL和符号。那 这就是为什么你应该使用时髦地图符号。​​value(consumer(…), producer(…))​​​​GString​

1.6. 常用顶级元素

以下部分介绍最常见的顶级元素:

  • 描述
  • 名字
  • 忽略合同
  • 进行中的合同
  • 从文件传递值
  • 元数据

1.6.1. 描述

您可以在合同中添加 a。说明是任意文本。这 以下代码显示了一个示例:​​description​

org.springframework.cloud.contract.spec.Contract.make {
description('''
given:
An input
when:
Sth happens
then:
Output
''')
}

1.6.2. 名称

您可以为合同提供名称。假定您提供以下名称:。如果这样做,则自动生成的测试的名称为。此外,WireMock 存根中的存根名称是。​​should register a user​​​​validate_should_register_a_user​​​​should_register_a_user.json​

必须确保名称不包含任何使 生成的测试未编译。另外,请记住,如果您为 多个合约,自动生成的测试无法编译,生成的存根 相互覆盖。

以下示例演示如何向协定添加名称:

org.springframework.cloud.contract.spec.Contract.make {
name("some_special_name")
}

1.6.3. 忽略合约

如果要忽略合同,可以在 插件配置或在合约本身上设置属性。以下 示例显示了如何执行此操作:​​ignored​

org.springframework.cloud.contract.spec.Contract.make {
ignored()
}

1.6.4. 进行中的合同

正在进行的合约不会在生产者端生成测试,但允许生成存根。

请谨慎使用此功能,因为它可能会导致误报,因为您会生成存根供使用者使用,而实际上没有实现。

如果要设置正在进行的合同,请执行以下操作 示例显示了如何执行此操作:

org.springframework.cloud.contract.spec.Contract.make {
inProgress()
}

您可以设置 Spring 云合约插件属性的值,以确保当源中至少有一个正在进行的合约时,您的构建会中断。​​failOnInProgress​

1.6.5. 从文件传递值

从版本开始,可以从文件传递值。假设您有 项目中的以下资源:​​1.2.0​

└── src
└── test
└── resources
└── contracts
├── readFromFile.groovy
├── request.json
└── response.json

进一步假设您的合同如下:

/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import org.springframework.cloud.contract.spec.Contract

Contract.make {
request {
method('PUT')
headers {
contentType(applicationJson())
}
body(file("request.json"))
url("/1")
}
response {
status OK()
body(file("response.json"))
headers {
contentType(applicationJson())
}
}
}

进一步假设 JSON 文件如下所示:

请求.json

响应.json

{
"status": "REQUEST"
}

当测试或存根生成发生时,and文件的内容将传递给正文 的请求或响应。文件名必须是某个位置中的文件 相对于合同所在的文件夹。​​request.json​​​​response.json​

如果需要以二进制形式传递文件的内容, 可以在编码的 DSL 中使用该方法,也可以在 YAML 中使用字段。​​fileAsBytes​​​​bodyFromFileAsBytes​

下面的示例演示如何传递二进制文件的内容:

import org.springframework.cloud.contract.spec.Contract

Contract.make {
request {
url("/1")
method(PUT())
headers {
contentType(applicationOctetStream())
}
body(fileAsBytes("request.pdf"))
}
response {
status 200
body(fileAsBytes("response.pdf"))
headers {
contentType(applicationOctetStream())
}
}
}

每当您想要使用二进制有效负载时,都应使用此方法, 对于HTTP和消息传递。

1.6.6. 元数据

您可以添加到您的合同中。通过元数据,您可以将配置传递给扩展。您可以在下面找到 使用密钥的示例。它的值是一个映射,其键是和值是WireMock的对象。春云合约能够 使用自定义代码修补生成的存根映射的部分。您可能希望这样做以添加网络钩子,自定义 延迟或与第三方 WireMock 扩展集成。​​metadata​​​​wiremock​​​​stubMapping​​​​StubMapping​

Contract.make {
request {
method GET()
url '/drunks'
}
response {
status OK()
body([
count: 100
])
headers {
contentType("application/json")
}
}
metadata([
wiremock: [
stubMapping: '''\
{
"response" : {
"fixedDelayMilliseconds": 2000
}
}
'''
]
])
}

在以下部分中,您可以找到支持的元数据条目的示例。

元数据amqp
  • .key:amqp
  • 描述:

基于 AMQP 的通信的元数据

例:

input:
messageProperties: null
connectToBroker:
additionalOptions: null
declareQueueWithName: null
outputMessage:
messageProperties: null
connectToBroker:
additionalOptions: null
declareQueueWithName: null

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.messaging.amqp.AmqpMetadata​
  • ​org.springframework.amqp.core.MessageProperties​
元数据独立
  • .key:standalone
  • 描述:

用于独立通信的元数据 - 使用正在运行的中间件

例:

setup:
options: null
input:
additionalOptions: null
outputMessage:
additionalOptions: null

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.messaging.camel.StandaloneMetadata​
元数据验证器Http
  • .key:verifierHttp
  • 描述:

框架使用的元数据条目

例:

scheme: "HTTP"
protocol: "HTTP_1_1"

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetaData​
元数据卡夫卡
  • .key:kafka
  • 描述:

基于 Kafka 的通信的元数据

例:

input:
connectToBroker:
additionalOptions: null
outputMessage:
connectToBroker:
additionalOptions: null

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.messaging.kafka.KafkaMetadata​
元数据线模拟
  • .key:wiremock
  • 描述:

用于扩展 WireMock 存根的元数据。

存根映射可以是以下类之一 [,,]。请查看wiremock.org/docs/stubbing/ 以获取有关 StubMapping 类属性的详细信息。​​String​​​​StubMapping​​​​Map​

例:

stubMapping: null

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.wiremock.WireMockMetaData​
  • ​com.github.tomakehurst.wiremock.stubbing.StubMapping​
元数据验证程序
  • .key:verifier
  • 描述:

框架使用的元数据条目

例:

tool: null

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.dsl.ContractVerifierMetadata​
元数据验证程序消息
  • .key:verifierMessage
  • 描述:

框架使用的内部元数据条目,与消息传递相关

例:

messageType: null

单击此处展开 JSON 架构:

 

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:

  • ​org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata​

2. HTTP 合约

Spring Cloud 合约允许您验证使用 REST 或 HTTP 作为 通讯手段。Spring 云合约验证,对于与 条件 从合同的一部分,服务器提供响应 与合同的部分保持一致。随后,合同用于 生成 WireMock 存根,对于与提供的条件匹配的任何请求,提供 适当的响应。​​request​​​​response​

2.1. HTTP 顶级元素

您可以在合约定义的顶级闭包中调用以下方法:

  • ​request​​:命令的
  • ​response​​:命令的
  • ​priority​​:自选

以下示例演示如何定义 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 {
method GET()
url "/foo"
//...
}

// 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 {
status 200
//...
}

// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
priority 1
}

如果你想让你的合同有更高的优先级, 您需要将较小的数字传递给标签或方法。例如,awith 值 OF 的优先级高于具有值 OF 的 A。​​priority​​​​priority​​​​5​​​​priority​​​​10​

2.2. HTTP请求

HTTP 协议只需要在请求中指定方法和 URL。这 在合同的请求定义中,相同的信息是强制性的。

以下示例显示了请求的协定:

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 {
//...
status 200
}
}

您可以指定绝对值而不是相对值,但是使用 推荐的方法,因为这样做会使测试独立于主机。​​url​​​​urlPath​

以下示例使用:​​url​

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 {
//...
status 200
}
}

​request​​可能包含查询参数,如以下示例(使用)所示:​​urlPath​

org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()

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 {
//...
status 200
}
}

​request​​可以包含其他请求标头,如以下示例所示:

org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"

// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
headers {
header 'key': 'value'
contentType(applicationJson())
}

//...
}

response {
//...
status 200
}
}

​request​​可能包含其他请求 Cookie,如以下示例所示:

org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"

// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
// there are also some helper methods
cookies {
cookie 'key': 'value'
cookie('another_key', 'another_value')
}

//...
}

response {
//...
status 200
}
}

​request​​可能包含请求正文,如以下示例所示:

org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"

// 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 {
//...
status 200
}
}

​request​​可以包含多部分元素。要包含多部分元素,请使用方法/部分,如以下示例所示:​​multipart​

org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/multipart'
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
// key (parameter name), value (parameter value) pair
formParameter: $(c(regex('".+"')), p('"formParameterValue"')),
someBooleanParameter: $(c(regex(anyBoolean())), p('true')),
// a named parameter (e.g. with `file` name) that represents file with
// `name` and `content`. You can also call `named("fileName", "fileContent")`
file: named(
// name of the file
name: $(c(regex(nonEmpty())), p('filename.csv')),
// content of the file
content: $(c(regex(nonEmpty())), p('file content')),
// content type for the part
contentType: $(c(regex(nonEmpty())), p('application/json')))
)
}
response {
status OK()
}
}
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
file: named(
name: value(stub(regex('.+')), test('file')),
content: value(stub(regex('.+')), test([100, 117, 100, 97] as byte[]))
)
)
}
response {
status 200
}
}

在前面的示例中,我们通过以下两种方式之一定义了参数:

编码的 DSL

  • 直接通过使用映射表示法,其中值可以是动态属性(例如)。formParameter: $(consumer(…​), producer(…​))
  • 通过使用允许您设置命名参数的方法。命名参数 可以设置和。您可以使用具有两个参数的方法调用它, 例如,或者通过使用地图表示法,例如。named(…​)namecontentnamed("fileName", "fileContent")named(name: "fileName", content: "fileContent")

亚姆

  • 多部分参数在部分中设置。multipart.params
  • 命名参数(给定参数名称) 可以在该部分中设置。该部分包含 (参数的名称)、(文件名)、(文件的内容)字段。fileNamefileContentmultipart.namedparamNamefileNamefileContent
  • 动态位可以在该部分中设置。matchers.multipart
  • 对于参数,请使用 thesection,它可以接受非正则表达式。paramsregexpredefined
  • 对于命名参数,请使用您首先所在的部分 定义参数名称。然后你可以通过 非正则表达中任一奥林AOR的参数化。namedparamNamefileNamefileContentregexpredefined

从前面示例中的协定中,生成的测试和存根如下所示:

// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "multipart/form-data;boundary=AaB03x")
.param("formParameter", "\"formParameterValue\"")
.param("someBooleanParameter", "true")
.multiPart("file", "filename.csv", "file content".getBytes());

// when:
ResponseOptions response = given().spec(request)
.put("/multipart");

// then:
assertThat(response.statusCode()).isEqualTo(200);

2.3. HTTP响应

响应必须包含 HTTP 状态代码,并且可能包含其他信息。这 以下代码显示了一个示例:

org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"
}
response {
// Status code sent by the server
// in response to request specified above.
status OK()
}
}

除了状态之外,响应还可能包含标头、cookie 和正文,它们是 指定方式与请求中的方式相同(请参阅HTTP 请求)。

在 Groovy DSL 中,您可以引用方法以提供有意义的状态而不是数字。例如,您可以调用 statusorfor。​​org.springframework.cloud.contract.spec.internal.HttpStatus​​​​OK()​​​​200​​​​BAD_REQUEST()​​​​400​

2.4. 动态属性

协定可以包含一些动态属性:时间戳、ID 等。你没有 想要强制消费者存根他们的时钟以始终返回相同的时间值 以便它与存根匹配。

对于Groovy DSL,您可以在合同中提供动态部分 通过两种方式:直接将它们传递到正文中或将它们设置在称为的单独部分中。​​bodyMatchers​

在 2.0.0 之前,这些是通过 usingand 设置的。 有关详细信息,请参阅迁移指南​。​​testMatchers​​​​stubMatchers​

对于 YAML,只能使用节。​​matchers​

其中的条目必须引用有效负载的现有元素。有关详细信息,请参阅此问题​。​​matchers​

2.4.1. 身体内部的动态特性

此部分仅对编码的 DSL(Groovy、Java 等)有效。有关类似功能的 YAML 示例,请参阅匹配器部分中的动态属性。

您可以使用方法设置正文中的属性,或者,如果您使用 时髦地图符号,与。以下示例演示如何设置动态 具有值方法的属性:​​value​​​​$()​

价值

$

value(consumer(...), producer(...))
value(c(...), p(...))
value(stub(...), test(...))
value(client(...), server(...))

这两种方法同样有效。Theand方法是方法的别名。后续部分将详细介绍可以对这些值执行哪些操作。​​stub​​​​client​​​​consumer​

2.4.2. 正则表达式

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参阅匹配器部分中的动态属性。

您可以使用正则表达式在协定 DSL 中编写请求。这样做是 当您想要指示应提供给定响应时特别有用 对于遵循给定模式的请求。此外,您可以在以下情况下使用正则表达式: 需要为测试和服务器端测试使用模式而不是精确值。

确保正则表达式匹配序列的整个区域,因为在内部调用 Pattern.matches()。例如,不匹配,但匹配。 还有其他几个已知的限制。​​abc​​​​aabc​​​​.abc​

以下示例演示如何使用正则表达式编写请求:

org.springframework.cloud.contract.spec.Contract.make {
request {
method('GET')
url $(consumer(~/\/[0-9]{2}/), producer('/12'))
}
response {
status OK()
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'
}
}
}

您还可以使用正则表达式仅提供通信的一端。如果你 这样做,然后合约引擎会自动提供生成的匹配字符串 提供的正则表达式。以下代码显示了 Groovy 的示例:

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 OK()
body([
responseElement: $(producer(regex('[0-9]{7}')))
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}

在前面的示例中,通信的另一端具有相应的数据 为请求和响应生成。

春云合约附带一系列预定义的正则表达式,您可以 在合同中使用,如以下示例所示:

public static RegexProperty onlyAlphaUnicode() {
return new RegexProperty(ONLY_ALPHA_UNICODE).asString();
}

public static RegexProperty alphaNumeric() {
return new RegexProperty(ALPHA_NUMERIC).asString();
}

public static RegexProperty number() {
return new RegexProperty(NUMBER).asDouble();
}

public static RegexProperty positiveInt() {
return new RegexProperty(POSITIVE_INT).asInteger();
}

public static RegexProperty anyBoolean() {
return new RegexProperty(TRUE_OR_FALSE).asBooleanType();
}

public static RegexProperty anInteger() {
return new RegexProperty(INTEGER).asInteger();
}

public static RegexProperty aDouble() {
return new RegexProperty(DOUBLE).asDouble();
}

public static RegexProperty ipAddress() {
return new RegexProperty(IP_ADDRESS).asString();
}

public static RegexProperty hostname() {
return new RegexProperty(HOSTNAME_PATTERN).asString();
}

public static RegexProperty email() {
return new RegexProperty(EMAIL).asString();
}

public static RegexProperty url() {
return new RegexProperty(URL).asString();
}

public static RegexProperty httpsUrl() {
return new RegexProperty(HTTPS_URL).asString();
}

public static RegexProperty uuid() {
return new RegexProperty(UUID).asString();
}

public static RegexProperty uuid4() {
return new RegexProperty(UUID4).asString();
}

public static RegexProperty isoDate() {
return new RegexProperty(ANY_DATE).asString();
}

public static RegexProperty isoDateTime() {
return new RegexProperty(ANY_DATE_TIME).asString();
}

public static RegexProperty isoTime() {
return new RegexProperty(ANY_TIME).asString();
}

public static RegexProperty iso8601WithOffset() {
return new RegexProperty(ISO8601_WITH_OFFSET).asString();
}

public static RegexProperty nonEmpty() {
return new RegexProperty(NON_EMPTY).asString();
}

public static RegexProperty nonBlank() {
return new RegexProperty(NON_BLANK).asString();
}

在您的合同中,您可以按如下方式使用它(例如 Groovy DSL):

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'))}]"
)
}
}

为了使事情更简单,您可以使用一组预定义的对象,这些对象会自动 假设您希望传递正则表达式。 所有这些方法都以前缀开头,如下所示:​​any​

T anyAlphaUnicode();

T anyAlphaNumeric();

T anyNumber();

T anyInteger();

T anyPositiveInt();

T anyDouble();

T anyHex();

T aBoolean();

T anyIpAddress();

T anyHostname();

T anyEmail();

T anyUrl();

T anyHttpsUrl();

T anyUuid();

T anyDate();

T anyDateTime();

T anyTime();

T anyIso8601WithOffset();

T anyNonBlankString();

T anyNonEmptyString();

T anyOf(String... values);

下面的示例演示如何引用这些方法:

Contract contractDsl = Contract.make {
name "foo"
label 'trigger_event'
input {
triggeredBy('toString()')
}
outputMessage {
sentTo 'topic.rateablequote'
body([
alpha : $(anyAlphaUnicode()),
number : $(anyNumber()),
anInteger : $(anyInteger()),
positiveInt : $(anyPositiveInt()),
aDouble : $(anyDouble()),
aBoolean : $(aBoolean()),
ip : $(anyIpAddress()),
hostname : $(anyHostname()),
email : $(anyEmail()),
url : $(anyUrl()),
httpsUrl : $(anyHttpsUrl()),
uuid : $(anyUuid()),
date : $(anyDate()),
dateTime : $(anyDateTime()),
time : $(anyTime()),
iso8601WithOffset: $(anyIso8601WithOffset()),
nonBlankString : $(anyNonBlankString()),
nonEmptyString : $(anyNonEmptyString()),
anyOf : $(anyOf('foo', 'bar'))
])
}
}
局限性

由于库的某些限制,它生成了一个字符串 正则表达式,如果您依赖自动,请不要在正则表达式中使用 theandsigns 代。请参阅问题 899​。​​Xeger​​​​$​​​​^​

不要将实例用作(例如,)的值。 它会导致 a. 使用代替。 请参阅问题 900​。​​LocalDate​​​​$​​​​$(consumer(LocalDate.now()))​​​​java.lang.StackOverflowError​​​​$(consumer(LocalDate.now().toString()))​

2.4.3. 传递可选参数

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参阅匹配器部分中的动态属性。

您可以在合同中提供可选参数。但是,您可以提供 可选参数仅适用于以下各项:

  • 请求的存根端
  • 响应的测试端

以下示例演示如何提供可选参数:

org.springframework.cloud.contract.spec.Contract.make {
priority 1
name "optionals"
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('https://partners.com'))
)
}
response {
status 404
headers {
header 'Content-Type': 'application/json'
}
body(
code: value(consumer("123123"), producer(optional("123123")))
)
}
}

通过使用该方法包裹身体的一部分,您可以创建一个规则 必须出现 0 次或更多次的表达式。​​optional()​

如果使用 Spock,将从前面的示例生成以下测试:

槽的

package com.example

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification
import io.restassured.response.ResponseOptions

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*

@SuppressWarnings("rawtypes")
class FooSpec extends Specification {

\tdef validate_optionals() throws Exception {
\t\tgiven:
\t\t\tMockMvcRequestSpecification request = given()
\t\t\t\t\t.header("Content-Type", "application/json")
\t\t\t\t\t.body('''{"email":"abc@abc.com","callback_url":"https://partners.com"}''')

\t\twhen:
\t\t\tResponseOptions response = given().spec(request)
\t\t\t\t\t.post("/users/password")

\t\tthen:
\t\t\tresponse.statusCode() == 404
\t\t\tresponse.header("Content-Type") == 'application/json'

\t\tand:
\t\t\tDocumentContext parsedJson = JsonPath.parse(response.body.asString())
\t\t\tassertThatJson(parsedJson).field("['code']").matches("(123123)?")
\t}

}

The following stub would also be generated:

'''
{
"request" : {
"url" : "/users/password",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6})?/)]"
}, {
"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
}
'''

2.4.4. 在服务器端调用自定义方法

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参阅匹配器部分中的动态属性。

可以定义在测试期间在服务器端运行的方法调用。这样的 方法可以添加到配置中定义的类中。这 以下代码显示了测试用例的协定部分的示例:​​baseClassForTests​

method GET()

以下代码显示了测试用例的基类部分:

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
}

}

不能同时使用这两个 aand 来执行串联。为 示例,调用引线 结果不当。相反,卡兰德 确保该方法返回您需要的所有内容。​​String​​​​execute​​​​header('Authorization', 'Bearer ' + execute('authToken()'))​​​​header('Authorization', execute('authToken()'))​​​​authToken()​

从 JSON 读取的对象类型可以是以下类型之一,具体取决于 JSON 路径:

  • ​String​​:如果指向 JSON 中的值。String
  • ​JSONArray​​:如果您指向 JSON。List
  • ​Map​​:如果您指向 JSON。Map
  • ​Number​​:如果指向 JSON 中的 、 和其他数字类型。IntegerDouble
  • ​Boolean​​:如果您指向 JSON。Boolean

在合同的请求部分,您可以指定应从 一种方法。​​body​

您必须同时提供使用者端和生产者端。该部分 适用于整个身体,而不是部分身体。​​execute​

以下示例演示如何从 JSON 读取对象:

Contract contractDsl = Contract.make {
request {
method 'GET'
url '/something'
body(
$(c('foo'), p(execute('hashCode()')))
)
}
response {
status OK()
}
}

前面的示例导致在请求正文中调用该方法。 它应类似于以下代码:​​hashCode()​

// given:
MockMvcRequestSpecification request = given()
.body(hashCode());

// when:
ResponseOptions response = given().spec(request)
.get("/something");

// then:
assertThat(response.statusCode()).isEqualTo(200);

2.4.5. 从响应中引用请求

最好的情况是提供固定值,但有时您需要引用 在您的回复中请求。

如果你在 Groovy DSL 中编写合约,你可以使用该方法,它允许 您引用了 HTTP 请求中的一堆元素。您可以使用以下内容 选项:​​fromRequest()​

  • ​fromRequest().url()​​:返回请求 URL 和查询参数。
  • ​fromRequest().query(String key)​​:返回具有给定名称的第一个查询参数。
  • ​fromRequest().query(String key, int index)​​:返回第 n 个查询参数,其中包含 名。
  • ​fromRequest().path()​​:返回完整路径。
  • ​fromRequest().path(int index)​​:返回第 n 个路径元素。
  • ​fromRequest().header(String key)​​:返回具有给定名称的第一个标头。
  • ​fromRequest().header(String key, int index)​​:返回具有给定名称的第 n 个标头。
  • ​fromRequest().body()​​:返回完整的请求正文。
  • ​fromRequest().body(String jsonPath)​​:从请求中返回元素 匹配 JSON 路径。

如果使用 YAML 合约定义或 Java 合约定义,则必须将Handlebars表示法与自定义 Spring Cloud 合约一起使用 函数来实现这一点。在这种情况下,您可以使用以下选项:​​{{{ }}}​

  • ​{{{ request.url }}}​​:返回请求 URL 和查询参数。
  • ​{{{ request.query.key.[index] }}}​​:返回具有给定名称的第 n 个查询参数。 例如,对于键,第一个条目是thing{{{ request.query.thing.[0] }}}
  • ​{{{ request.path }}}​​:返回完整路径。
  • ​{{{ request.path.[index] }}}​​:返回第 n 个路径元素。例如 第一个条目是{{{ request.path.[0] }}}`
  • ​{{{ request.headers.key }}}​​:返回具有给定名称的第一个标头。
  • ​{{{ request.headers.key.[index] }}}​​:返回具有给定名称的第 n 个标头。
  • ​{{{ request.body }}}​​:返回完整的请求正文。
  • ​{{{ jsonpath this 'your.json.path' }}}​​:从请求中返回元素 匹配 JSON 路径。例如,对于 JSON 路径,请使用$.here{{{ jsonpath this '$.here' }}}

请考虑以下合同:

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 OK()
headers {
header(authorization(), "foo ${fromRequest().header(authorization())} bar")
}
body(
url: fromRequest().url(),
path: fromRequest().path(),
pathIndex: fromRequest().path(1),
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",
rawUrl: fromRequest().rawUrl(),
rawPath: fromRequest().rawPath(),
rawPathIndex: fromRequest().rawPath(1),
rawParam: fromRequest().rawQuery('foo'),
rawParamIndex: fromRequest().rawQuery('foo', 1),
rawAuthorization: fromRequest().rawHeader('Authorization'),
rawAuthorization2: fromRequest().rawHeader('Authorization', 1),
rawResponseFoo: fromRequest().rawBody('$.foo'),
rawResponseBaz: fromRequest().rawBody('$.baz'),
rawResponseBaz2: "Bla bla ${fromRequest().rawBody('$.foo')} bla bla"
)
}
}
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 OK()
headers {
contentType(applicationJson())
}
body('''
{
"responseFoo": "{{{ jsonPath request.body '$.foo' }}}",
"responseBaz": {{{ jsonPath request.body '$.baz' }}},
"responseBaz2": "Bla bla {{{ jsonPath request.body '$.foo' }}} bla bla"
}
'''.toString())
}
}

运行 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("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret");
assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2");
assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx");
assertThatJson(parsedJson).field("['param']").isEqualTo("bar");
assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2");
assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1");
assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5);
assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar");
assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2");
assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla");

如您所见,请求中的元素已在响应中正确引用。

生成的 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" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}",
"headers" : {
"Authorization" : "{{{request.headers.Authorization.[0]}}};foo"
},
"transformers" : [ "response-template" ]
}
}

发送请求,例如合同结果部分中提出的请求 在发送以下响应正文时:​​request​

{
"url" : "/api/v1/xxxx?foo=bar&foo=bar2",
"path" : "/api/v1/xxxx",
"pathIndex" : "v1",
"param" : "bar",
"paramIndex" : "bar2",
"authorization" : "secret",
"authorization2" : "secret2",
"fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
"responseFoo" : "bar",
"responseBaz" : 5,
"responseBaz2" : "Bla bla bar bla bla"
}

此功能仅适用于大于或等于的 WireMock 版本 到 2.5.1.Spring Cloud 合约验证器使用 WireMock 的响应转换器。它使用车把将胡子模板转换为 正确的值。此外,它还注册两个帮助程序函数:​​response-template​​​​{{{ }}}​

  • ​escapejsonbody​​:以可嵌入 JSON 的格式转义请求正文。
  • ​jsonpath​​:对于给定参数,在请求正文中查找对象。

2.4.6. 匹配器部分中的动态属性

如果您使用Pact,以下讨论可能看起来很熟悉。 相当多的用户习惯于在身体和设置之间分离 合同的动态部分。

您可以使用该部分有两个原因:​​bodyMatchers​

  • 定义应以存根结尾的动态值。 您可以在合同的理论部分设置它。requestinputMessage
  • 验证测试结果。 本节存在于 合同。responseoutputMessage

目前,Spring Cloud 合约验证器仅支持基于 JSON 路径的匹配器,具有 以下匹配可能性:

编码的 DSL

对于存根(在消费者端的测试中):

  • ​byEquality()​​:从提供的 JSON 路径中的使用者请求中获取的值必须是 等于合同中规定的价值。
  • ​byRegex(…)​​:从提供的 JSON 路径中的使用者请求中获取的值必须 匹配正则表达式。还可以传递预期匹配值的类型(例如,,,等)。asString()asLong()
  • ​byDate()​​:从提供的 JSON 路径中的使用者请求中获取的值必须 匹配 ISO 日期值的正则表达式。
  • ​byTimestamp()​​:从提供的 JSON 路径中的使用者请求中获取的值必须 匹配 ISO 日期时间值的正则表达式。
  • ​byTime()​​:从提供的 JSON 路径中的使用者请求中获取的值必须 匹配 ISO 时间值的正则表达式。

对于验证(在生产者方生成的测试中):

  • ​byEquality()​​:在提供的 JSON 路径中从生产者的响应中获取的值必须是 等于合同中提供的价值。
  • ​byRegex(…)​​:在提供的 JSON 路径中从生产者的响应中获取的值必须 匹配正则表达式。
  • ​byDate()​​:从提供的 JSON 路径中的生产者响应中获取的值必须匹配 ISO 日期值的正则表达式。
  • ​byTimestamp()​​:在提供的 JSON 路径中从生产者的响应中获取的值必须 匹配 ISO 日期时间值的正则表达式。
  • ​byTime()​​:从提供的 JSON 路径中的生产者响应中获取的值必须匹配 ISO 时间值的正则表达式。
  • ​byType()​​:在提供的 JSON 路径中,从生产者的响应中获取的值需要为 与协定中响应正文中定义的类型相同。可以采用闭包,您可以在其中设置和。对于 请求端,则应使用闭包来断言集合的大小。 这样,就可以断言平展集合的大小。检查 未平展集合,请使用自定义方法。byTypeminOccurrencemaxOccurrencebyCommand(…​)testMatcher
  • ​byCommand(…)​​:从提供的 JSON 路径中的生产者响应中获取的值为 作为输入传递给您提供的自定义方法。例如,结果调用一个方法,其值与 JSON 路径被传递。从 JSON 读取的对象类型可以是 以下内容,具体取决于 JSON 路径:byCommand('thing($it)')thing
  • ​String​​:如果指向值。String
  • ​JSONArray​​:如果您指向 a。List
  • ​Map​​:如果您指向 a。Map
  • ​Number​​:如果您指向 、 或其他类型的数字。IntegerDouble
  • ​Boolean​​:如果您指向 a。Boolean
  • ​byNull()​​:从提供的 JSON 路径中的响应中获取的值必须为 null。
亚姆

有关 Groovy 的详细说明,请参阅 Groovy 部分 类型的含义。

对于 YAML,匹配器的结构类似于以下示例:

- path: $.thing1
type: by_regex
value: thing2
regexType: as_string

或者,如果要使用预定义的正则表达式之一,可以使用类似于以下示例的内容:​​[only_alpha_unicode, number, any_boolean, ip_address, hostname, email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]​

- path: $.thing1
type: by_regex
predefined: only_alpha_unicode

以下列表显示了允许的值列表:​​type​

  • 为:stubMatchers
  • ​by_equality​
  • ​by_regex​
  • ​by_date​
  • ​by_timestamp​
  • ​by_time​
  • ​by_type​
  • 接受两个附加字段(和)。minOccurrencemaxOccurrence
  • 为:testMatchers
  • ​by_equality​
  • ​by_regex​
  • ​by_date​
  • ​by_timestamp​
  • ​by_time​
  • ​by_type​
  • 接受两个附加字段(和)。minOccurrencemaxOccurrence
  • ​by_command​
  • ​by_null​

您还可以在字段中定义正则表达式对应的类型。以下列表显示了允许的正则表达式类型:​​regexType​

  • ​as_integer​
  • ​as_double​
  • ​as_float​
  • ​as_long​
  • ​as_short​
  • ​as_boolean​
  • ​as_string​

请考虑以下示例:

Contract contractDsl = Contract.make {
request {
method 'GET'
urlPath '/get'
body([
duck : 123,
alpha : 'abc',
number : 123,
aBoolean : true,
date : '2017-01-01',
dateTime : '2017-01-01T01:23:45',
time : '01:02:34',
valueWithoutAMatcher: 'foo',
valueWithTypeMatch : 'string',
key : [
'complex.key': 'foo'
]
])
bodyMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()).asInteger())
jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
response {
status OK()
body([
duck : 123,
alpha : 'abc',
number : 123,
positiveInteger : 1234567890,
negativeInteger : -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
aBoolean : true,
date : '2017-01-01',
dateTime : '2017-01-01T01:23:45',
time : "01:02:34",
valueWithoutAMatcher : 'foo',
valueWithTypeMatch : 'string',
valueWithMin : [
1, 2, 3
],
valueWithMax : [
1, 2, 3
],
valueWithMinMax : [
1, 2, 3
],
valueWithMinEmpty : [],
valueWithMaxEmpty : [],
key : [
'complex.key': 'foo'
],
nullValue : null
])
bodyMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
// asserts the jsonpath value against the provided value
jsonPath('$.duck', byEquality())
// asserts the jsonpath value against some default regex
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()).asInteger())
jsonPath('$.positiveInteger', byRegex(anInteger()).asInteger())
jsonPath('$.negativeInteger', byRegex(anInteger()).asInteger())
jsonPath('$.positiveDecimalNumber', byRegex(aDouble()).asDouble())
jsonPath('$.negativeDecimalNumber', byRegex(aDouble()).asDouble())
jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
// asserts vs inbuilt time related regex
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
// asserts that the resulting type is the same as in response body
jsonPath('$.valueWithTypeMatch', byType())
jsonPath('$.valueWithMin', byType {
// results in verification of size of array (min 1)
minOccurrence(1)
})
jsonPath('$.valueWithMax', byType {
// results in verification of size of array (max 3)
maxOccurrence(3)
})
jsonPath('$.valueWithMinMax', byType {
// results in verification of size of array (min 1 & max 3)
minOccurrence(1)
maxOccurrence(3)
})
jsonPath('$.valueWithMinEmpty', byType {
// results in verification of size of array (min 0)
minOccurrence(0)
})
jsonPath('$.valueWithMaxEmpty', byType {
// results in verification of size of array (max 0)
maxOccurrence(0)
})
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
jsonPath('$.nullValue', byNull())
}
headers {
contentType(applicationJson())
header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
}
}
}

在前面的示例中,您可以在章节中看到合同的动态部分。对于请求部分,您可以看到,对于所有字段,但存根应具有的正则表达式的值 包含是显式设置的。对于,验证发生 与不使用匹配器的方式相同。在这种情况下,测试将执行 相等性检查。​​matchers​​​​valueWithoutAMatcher​​​​valueWithoutAMatcher​

对于本节中的响应端,我们在 类似的方式。唯一的区别是匹配者也存在。这 验证程序引擎检查四个字段以验证来自测试的响应 具有 JSON 路径与给定字段匹配的值,与该值的类型相同 在响应正文中定义,并通过以下检查(基于要调用的方法):​​bodyMatchers​​​​byType​

  • 对于,引擎检查类型是否相同。$.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\",\"key\":{\"complex.key\":\"foo\"}}");

// 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+|\\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((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3);
assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0);
assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
assertThatValueIsANumber(parsedJson.read("$.duck"));
assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");

请注意,对于方法,该示例调用。此方法必须在测试基类中定义,或者 静态导入到测试中。请注意,调用已转换为。这意味着发动机花了 方法名称,并将正确的 JSON 路径作为参数传递给它。​​byCommand​​​​assertThatValueIsANumber​​​​byCommand​​​​assertThatValueIsANumber(parsedJson.read("$.duck"));​

生成的 WireMock 存根在以下示例中:

'''
{
"request" : {
"urlPath" : "/get",
"method" : "POST",
"headers" : {
"Content-Type" : {
"matches" : "application/json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]"
}, {
"matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]"
}, {
"matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]"
}, {
"matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]"
}, {
"matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]"
}, {
"matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
}, {
"matchesJsonPath" : "$[?(@.duck == 123)]"
}, {
"matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
}, {
"matchesJsonPath" : "$[?(@.alpha == 'abc')]"
}, {
"matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\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 =~ /(.*)/)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]"
} ]
},
"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],\\"valueWithOccurrence\\":[1,2,3,4]}",
"headers" : {
"Content-Type" : "application/json"
},
"transformers" : [ "response-template", "spring-cloud-contract" ]
}
}
'''

如果使用 a,则请求和响应中具有 JSON 路径的地址的部分将从断言中删除。在以下情况下: 验证集合时,必须为 收集。​​matcher​​​​matcher​

请考虑以下示例:

Contract.make {
request {
method 'GET'
url("/foo")
}
response {
status OK()
body(events: [[
operation : 'EXPORT',
eventId : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
status : 'OK'
], [
operation : 'INPUT_PROCESSING',
eventId : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
status : 'OK'
]
]
)
bodyMatchers {
jsonPath('$.events[0].operation', byRegex('.+'))
jsonPath('$.events[0].eventId', byRegex('^([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})$'))
jsonPath('$.events[0].status', byRegex('.+'))
}
}
}

前面的代码导致创建以下测试(代码块仅显示断言部分):

and:
DocumentContext parsedJson = JsonPath.parse(response.body.asString())
assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99")
assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT")
assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING")
assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a")
assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK")
and:
assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+")
assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([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})\$")
assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+")

请注意,断言格式不正确。只有数组的第一个元素得到 断言。若要解决此问题,请将断言应用于整个集合并使用方法断言它。​​$.events​​​​byCommand(…)​

2.5. 异步支持

特别声明:以上内容(图片及文字)均为互联网收集或者用户上传发布,本站仅提供信息存储服务!如有侵权或有涉及法律问题请联系我们。
举报
评论区(0)
按点赞数排序
用户头像
精选文章
thumb 中国研究员首次曝光美国国安局顶级后门—“方程式组织”
thumb 俄乌线上战争,网络攻击弥漫着数字硝烟
thumb 从网络安全角度了解俄罗斯入侵乌克兰的相关事件时间线
下一篇
使用 Spring Cloud 合约 2022-12-07 03:01:34