Dynamic properties
-
testMatchers
-
regular_expressions
-
Groovy_DSL
-
YAML
-
JsonPath
-
Spring_Cloud_Contract_Verifier
-
wireMock :description: Spring Cloud Contract 的契约可以包含动态属性,如时间戳和 ID。这些属性可以通过两种方式提供:直接写入正文或放入名为“bodyMatchers”的单独部分。在 YAML 中,只能使用“matchers”部分。
Groovy DSL 中的动态属性可以使用 value
方法或 Groovy 映射表示法($()
)设置。这些方法适用于 stub、client 和 consumer 等通信端。
Groovy DSL 还支持使用正则表达式编写请求,这对指定模式匹配的请求非常有用。内置预定义正则表达式,可以通过 any
前缀的正则表达式创建函数调用简化使用。
契约中可以提供可选参数,但仅针对请求的存根端和响应的测试端。
Groovy DSL 中还可以调用服务器端的自定义方法,并在测试期间执行一个方法调用。
契约的请求部分可以从一个方法中获取正文,这允许从 JSON 中读取对象。
契约的响应部分可以使用 fromRequest() 方法从请求中引用元素,如 URL、查询参数、标头和正文。在 YAML 契约定义中,需要使用 Handlebars 表示法与 Spring Cloud 合同函数结合使用。
动态属性还可以通过 bodyMatchers 部分提供,目前仅支持基于 JSON 路径的匹配器。Groovy DSL 支持各种类型,如 byEquality、byRegex 等,而 YAML 使用 by_equality、by_regex 等类似结构。可以在 testMatchers 和 stubMatchers 中使用它们。
契约可以包含一些动态属性:时间戳、ID等。您不想强制消费者存根其时钟来始终返回相同的时间值,以便存根与之匹配。
The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not want to force the consumers to stub their clocks to always return the same value of time so that it gets matched by the stub.
对于Groovy DSL,您可以在契约中通过两种方式提供动态部分:直接将它们传递到正文中或将它们设置在一个称为`bodyMatchers`的单独部分中。
For the Groovy DSL, you can provide the dynamic parts in your contracts
in two ways: pass them directly in the body or set them in a separate section called
bodyMatchers
.
2.0.0 之前,这些是使用 |
Before 2.0.0, these were set by using |
对于YAML,您只能使用`matchers`部分。
For YAML, you can use only the matchers
section.
matchers
中的条目必须引用有效载荷中的现有元素。有关更多信息,请参阅 this issue。
Entries inside the matchers
must reference existing elements of the payload. For more information, see this issue.
Dynamic Properties inside the Body
本节只对编码的 DSL(Groovy、Java 等)有效。请参阅Dynamic Properties in the Matchers Sections 部分,了解类似特性的 YAML 示例。
This section is valid only for the Coded DSL (Groovy, Java, and so on). See the Dynamic Properties in the Matchers Sections section for YAML examples of a similar feature.
您可以使用`value`方法在正文内设置属性,或者,如果您使用Groovy映射表示法,则可以使用`$()`设置属性。以下示例显示了如何使用`value`方法设置动态属性:
You can set the properties inside the body either with the value
method or, if you use
the Groovy map notation, with $()
. The following example shows how to set dynamic
properties with the value method:
value(consumer(...), producer(...))
value(c(...), p(...))
value(stub(...), test(...))
value(client(...), server(...))
$(consumer(...), producer(...))
$(c(...), p(...))
$(stub(...), test(...))
$(client(...), server(...))
两种方法同样适用。`stub`和`client`方法是`consumer`方法的别名。后面的部分将仔细介绍您可以对这些值执行哪些操作。
Both approaches work equally well. The stub
and client
methods are aliases over the consumer
method. Subsequent sections take a closer look at what you can do with those values.
Regular Expressions
本节只对 Groovy DSL 有效。请参阅Dynamic Properties in the Matchers Sections 部分,了解类似特性的 YAML 示例。
This section is valid only for the Groovy DSL. See the Dynamic Properties in the Matchers Sections section for YAML examples of a similar feature.
您可以在契约DSL中使用正则表达式来编写请求。当您想要指定应该为遵循给定模式的请求提供给定的响应时,这样做尤其有用。此外,当您需要对测试和服务器端测试同时使用模式而不用具体值时,可以使用正则表达式。
You can use regular expressions to write your requests in the contract DSL. Doing so is particularly useful when you want to indicate that a given response should be provided for requests that follow a given pattern. Also, you can use regular expressions when you need to use patterns and not exact values both for your tests and your server-side tests.
确保正则表达式符合一个序列的整个区域,因为在内部会调用 Pattern.matches()
。例如,abc`不符合`aabc
,但`.abc`符合。还有一些其他known limitations。
Make sure that regex matches a whole region of a sequence, as, internally,
Pattern.matches()
is called. For instance, abc
does not match aabc
, but .abc
does.
There are several additional known limitations as well.
以下示例显示了如何使用正则表达式来编写请求:
The following example shows how to use regular expressions to write a request:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]
您还可以仅使用正则表达式提供通信的一方。如果您这样做,则契约引擎将自动提供与所提供的正则表达式匹配的生成字符串。以下代码显示了Groovy的示例:
You can also provide only one side of the communication with a regular expression. If you do so, then the contract engine automatically provides the generated string that matches the provided regular expression. The following code shows an example for Groovy:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[]
在上一个示例中,通信的另一方为请求和响应生成了各自的数据。
In the preceding example, the opposite side of the communication has the respective data generated for request and response.
Spring Cloud Contract带有您可以在契约中使用的一系列预定义正则表达式,如下面的示例所示:
Spring Cloud Contract comes with a series of predefined regular expressions that you can use in your contracts, as the following example shows:
Unresolved directive in dsl-dynamic-properties.adoc - include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexPatterns.java[]
在您的契约中,您可以按如下方式使用它(Groovy DSL的示例):
In your contract, you can use it as follows (example for the Groovy DSL):
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[]
为了更简单,您可以使用一组预定义的对象,它们会自动解析希望传递正则表达式。所有这些方法都使用`any`前缀开头,如下所示:
To make matters even simpler, you can use a set of predefined objects that automatically
assume that you want a regular expression to be passed.
All of those methods start with the any
prefix, as follows:
Unresolved directive in dsl-dynamic-properties.adoc - include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexCreatingProperty.java[]
以下示例显示了如何引用这些方法:
The following example shows how you can reference those methods:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]
Limitations
由于生成字符串时`Xeger` 库的某些限制,如果您依赖于自动生成,请勿在正则表达式中使用`$` 和`^` 符号。请参阅 Issue 899。
Due to certain limitations of the Xeger
library that generates a string out of
a regex, do not use the $
and ^
signs in your regex if you rely on automatic
generation. See Issue 899.
不要将 LocalDate
实例用作 $
的值(例如 $(consumer(LocalDate.now()))
)。这会造成 java.lang.StackOverflowError
。改用 $(consumer(LocalDate.now().toString()))
。请参见 Issue 900。
Do not use a LocalDate
instance as a value for $
(for example, $(consumer(LocalDate.now()))
).
It causes a java.lang.StackOverflowError
. Use $(consumer(LocalDate.now().toString()))
instead.
See Issue 900.
Passing Optional Parameters
此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参见 Dynamic Properties in the Matchers Sections 部分。
This section is valid only for Groovy DSL. See the Dynamic Properties in the Matchers Sections section for YAML examples of a similar feature.
可以在契约中提供可选参数。但是,你只能为以下内容提供可选参数:
You can provide optional parameters in your contract. However, you can provide optional parameters only for the following:
-
The STUB side of the Request
-
The TEST side of the Response
以下示例演示如何提供可选参数:
The following example shows how to provide optional parameters:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]
通过用 optional()
方法包装一部分内容,你创建了一个必须出现 0 次或更多次的正则表达式。
By wrapping a part of the body with the optional()
method, you create a regular
expression that must be present 0 or more times.
如果你使用 Spock,则会从上一个示例生成以下测试:
If you use Spock, the following test would be generated from the previous example:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
还将生成以下存根:
The following stub would also be generated:
Unresolved directive in dsl-dynamic-properties.adoc - include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[]
Calling Custom Methods on the Server Side
本节只对 Groovy DSL 有效。请参阅Dynamic Properties in the Matchers Sections 部分,了解类似特性的 YAML 示例。
This section is valid only for the Groovy DSL. See the Dynamic Properties in the Matchers Sections section for YAML examples of a similar feature.
可以在测试期间在服务器端运行定义一个方法调用。可以在配置中将此方法添加到定义为 baseClassForTests
的类中。以下代码显示了测试用例的契约部分示例:
You can define a method call that runs on the server side during the test. Such a
method can be added to the class defined as baseClassForTests
in the configuration. The
following code shows an example of the contract portion of the test case:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]
以下代码显示了测试用例的基本类部分:
The following code shows the base class portion of the test case:
Unresolved directive in dsl-dynamic-properties.adoc - include::{plugins_path}/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy[]
您无法同时使用 String
和 execute
来执行连接。例如,调用 header('Authorization', 'Bearer ' + execute('authToken()'))
将导致结果不正确。而是调用 header('Authorization', execute('authToken()'))
并确保 authToken()
方法返回您需要的一切。
You cannot use both a String
and execute
to perform concatenation. For
example, calling header('Authorization', 'Bearer ' + execute('authToken()'))
leads to
improper results. Instead, call header('Authorization', execute('authToken()'))
and
ensure that the authToken()
method returns everything you need.
对象从 JSON 中读取的类型可以是以下之一,这取决于 JSON 路径:
The type of the object read from the JSON can be one of the following, depending on the JSON path:
-
String
: If you point to aString
value in the JSON. -
JSONArray
: If you point to aList
in the JSON. -
Map
: If you point to aMap
in the JSON. -
Number
: If you point toInteger
,Double
, and other numeric type in the JSON. -
Boolean
: If you point to aBoolean
in the JSON.
在契约的请求部分中,可以指定 body
应该从一个方法中获取。
In the request part of the contract, you can specify that the body
should be taken from
a method.
您必须提供消费者端和生产者端。execute
部分应用于整个主体,不应用于部分。
You must provide both the consumer and the producer side. The execute
part
is applied for the whole body, not for parts of it.
以下示例演示如何从 JSON 中读取一个对象:
The following example shows how to read an object from JSON:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy[]
前面的示例会导致在请求正文中调用 hashCode()
方法。它应该类似于以下代码:
The preceding example results in calling the hashCode()
method in the request body.
It should resemble the following code:
// given:
MockMvcRequestSpecification request = given()
.body(hashCode());
// when:
ResponseOptions response = given().spec(request)
.get("/something");
// then:
assertThat(response.statusCode()).isEqualTo(200);
Referencing the Request from the Response
最好的情况是提供固定值,但有时候你需要在响应中引用一个请求。
The best situation is to provide fixed values, but sometimes you need to reference a request in your response.
如果你在 Groovy DSL 中编写契约,则可以使用 fromRequest()
方法,该方法允许你从 HTTP 请求中引用一堆元素。可以使用以下选项:
If you write contracts in the Groovy DSL, you can use the fromRequest()
method, which lets
you reference a bunch of elements from the HTTP request. You can use the following
options:
-
fromRequest().url()
: Returns the request URL and query parameters. -
fromRequest().query(String key)
: Returns the first query parameter with the given name. -
fromRequest().query(String key, int index)
: Returns the nth query parameter with the given name. -
fromRequest().path()
: Returns the full path. -
fromRequest().path(int index)
: Returns the nth path element. -
fromRequest().header(String key)
: Returns the first header with the given name. -
fromRequest().header(String key, int index)
: Returns the nth header with the given name. -
fromRequest().body()
: Returns the full request body. -
fromRequest().body(String jsonPath)
: Returns the element from the request that matches the JSON Path.
如果您使用 YAML 合同定义或 Java 定义,则必须使用 {{{ }}}
Handlebars 表示法和自定义 Spring Cloud Contract 函数来实现此目的。在这种情况下,您可以使用以下选项:
If you use the YAML contract definition or the Java one, you have to use the
Handlebars {{{ }}}
notation with custom Spring Cloud Contract
functions to achieve this. In that case, you can use the following options:
-
{{{ request.url }}}
: Returns the request URL and query parameters. -
{{{ request.query.key.[index] }}}
: Returns the nth query parameter with the given name. For example, for a key ofthing
, the first entry is{{{ request.query.thing.[0] }}}
-
{{{ request.path }}}
: Returns the full path. -
{{{ request.path.[index] }}}
: Returns the nth path element. For example, the first entry is`
{{{ request.path.[0] }}} -
{{{ request.headers.key }}}
: Returns the first header with the given name. -
{{{ request.headers.key.[index] }}}
: Returns the nth header with the given name. -
{{{ request.body }}}
: Returns the full request body. -
{{{ jsonpath this 'your.json.path' }}}
: Returns the element from the request that matches the JSON Path. For example, for a JSON path of$.here
, use{{{ jsonpath this '$.here' }}}
考虑以下契约:
Consider the following contract:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/yml/contract_reference_request.yml[]
package contracts.beer.rest;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.map;
class shouldReturnStatsForAUser implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method("POST");
r.url("/stats");
r.body(map().entry("name", r.anyAlphaUnicode()));
r.headers(h -> {
h.contentType(h.applicationJson());
});
});
c.response(r -> {
r.status(r.OK());
r.body(map()
.entry("text",
"Dear {{{jsonPath request.body '$.name'}}} thanks for your interested in drinking beer")
.entry("quantity", r.$(r.c(5), r.p(r.anyNumber()))));
r.headers(h -> {
h.contentType(h.applicationJson());
});
});
});
}
}
package contracts.beer.rest
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
contract {
request {
method = method("POST")
url = url("/stats")
body(mapOf(
"name" to anyAlphaUnicode
))
headers {
contentType = APPLICATION_JSON
}
}
response {
status = OK
body(mapOf(
"text" to "Don't worry $\{fromRequest().body("$.name")} thanks for your interested in drinking beer",
"quantity" to v(c(5), p(anyNumber))
))
headers {
contentType = fromRequest().header(CONTENT_TYPE)
}
}
}
运行 JUnit 测试生成会生成类似于以下示例的测试:
Running a JUnit test generation leads to a test that resembles the following example:
// 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");
如你所见,来自请求的元素已在响应中得到了正确引用。
As you can see, elements from the request have been properly referenced in the response.
生成的 WireMock 存根应类似于以下示例:
The generated WireMock stub should resemble the following example:
{
"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
部分中给出的请求,会导致发送以下响应正文:
Sending a request such as the one presented in the request
part of the contract results
in sending the following response body:
{
"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 Contract Verifier 使用 WireMock 的 response-template
响应转换器。它使用 Handlebars 将 Mustache {{{ }}}
模板转换为合适的值。此外,它注册了两个帮助函数:
This feature works only with WireMock versions greater than or equal
to 2.5.1. The Spring Cloud Contract Verifier uses WireMock’s
response-template
response transformer. It uses Handlebars to convert the Mustache {{{ }}}
templates into
proper values. Additionally, it registers two helper functions:
-
escapejsonbody
: Escapes the request body in a format that can be embedded in JSON. -
jsonpath
: For a given parameter, finds an object in the request body.
Dynamic Properties in the Matchers Sections
如果您使用 Pact,以下讨论可能看起来很熟悉。很多用户习惯于将合同的动态部分与主体分开设置。
If you work with Pact, the following discussion may seem familiar. Quite a few users are used to having a separation between the body and setting the dynamic parts of a contract.
你可以出于两个原因使用 bodyMatchers
部分:
You can use the bodyMatchers
section for two reasons:
-
Define the dynamic values that should end up in a stub. You can set it in the
request
part of your contract. -
Verify the result of your test. This section is present in the
response
oroutputMessage
side of the contract.
目前,Spring Cloud Contract Verifier 仅支持基于 JSON 路径的匹配器,具有以下匹配可能性:
Currently, Spring Cloud Contract Verifier supports only JSON path-based matchers with the following matching possibilities:
Coded DSL
对于存根(在消费者端的测试中):
For the stubs (in tests on the consumer’s side):
-
byEquality()
: The value taken from the consumer’s request in the provided JSON path must be equal to the value provided in the contract. -
byRegex(…)
: The value taken from the consumer’s request in the provided JSON path must match the regex. You can also pass the type of the expected matched value (for example,asString()
,asLong()
, and so on). -
byDate()
: The value taken from the consumer’s request in the provided JSON path must match the regex for an ISO Date value. -
byTimestamp()
: The value taken from the consumer’s request in the provided JSON path must match the regex for an ISO DateTime value. -
byTime()
: The value taken from the consumer’s request in the provided JSON path must match the regex for an ISO Time value.
对于验证(在生产者端的生成测试中):
For the verification (in generated tests on the Producer’s side):
-
byEquality()
: The value taken from the producer’s response in the provided JSON path must be equal to the provided value in the contract. -
byRegex(…)
: The value taken from the producer’s response in the provided JSON path must match the regex. -
byDate()
: The value taken from the producer’s response in the provided JSON path must match the regex for an ISO Date value. -
byTimestamp()
: The value taken from the producer’s response in the provided JSON path must match the regex for an ISO DateTime value. -
byTime()
: The value taken from the producer’s response in the provided JSON path must match the regex for an ISO Time value. -
byType()
: The value taken from the producer’s response in the provided JSON path needs to be of the same type as the type defined in the body of the response in the contract.byType
can take a closure, in which you can setminOccurrence
andmaxOccurrence
. For the request side, you should use the closure to assert size of the collection. That way, you can assert the size of the flattened collection. To check the size of an unflattened collection, use a custom method with thebyCommand(…)
testMatcher
. -
byCommand(…)
: The value taken from the producer’s response in the provided JSON path is passed as an input to the custom method that you provide. For example,byCommand('thing($it)')
results in calling athing
method to which the value matching the JSON Path gets passed. The type of the object read from the JSON can be one of the following, depending on the JSON path:-
String
: If you point to aString
value. -
JSONArray
: If you point to aList
. -
Map
: If you point to aMap
. -
Number
: If you point toInteger
,Double
, or another kind of number. -
Boolean
: If you point to aBoolean
.
-
-
byNull()
: The value taken from the response in the provided JSON path must be null.
YAML
请参阅 Groovy 部分,详细了解类型的含义。 |
See the Groovy section for a detailed explanation of what the types mean. |
对于 YAML,匹配器的结构类似于以下示例:
For YAML, the structure of a matcher resembles the following example:
- 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]
,则可以使用类似于以下示例的内容:
Alternatively, if you want to use one of the predefined regular expressions
[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]
, you can use something similar to the following example:
- path: $.thing1
type: by_regex
predefined: only_alpha_unicode
以下列表显示了允许的 type
值列表:
The following list shows the allowed list of type
values:
-
For
stubMatchers
:-
by_equality
-
by_regex
-
by_date
-
by_timestamp
-
by_time
-
by_type
-
Two additional fields (
minOccurrence
andmaxOccurrence
) are accepted.
-
-
-
For
testMatchers
:-
by_equality
-
by_regex
-
by_date
-
by_timestamp
-
by_time
-
by_type
-
Two additional fields (
minOccurrence
andmaxOccurrence
) are accepted.
-
-
by_command
-
by_null
-
你还可以定义正则表达式在 regexType
字段中对应于哪种类型。以下列表显示了允许的正则表达式类型:
You can also define which type the regular expression corresponds to in the regexType
field. The following list shows the allowed regular expression types:
-
as_integer
-
as_double
-
as_float
-
as_long
-
as_short
-
as_boolean
-
as_string
请考虑以下示例:
Consider the following example:
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy[]
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/yml/contract_matchers.yml[]
在前一个示例中,你可以在 matchers
部分看到合同的动态部分。对于请求部分,你可以看到,对于所有字段而非 valueWithoutAMatcher
,存根应当包含的正则表达式的值被明确设置。对于 valueWithoutAMatcher
,验证像不使用匹配器一样进行。在这种情况下,测试执行相等性检查。
In the preceding example, you can see the dynamic portions of the contract in the
matchers
sections. For the request part, you can see that, for all fields but
valueWithoutAMatcher
, the values of the regular expressions that the stub should
contain are explicitly set. For valueWithoutAMatcher
, the verification takes place
in the same way as without the use of matchers. In that case, the test performs an
equality check.
对于 bodyMatchers
部分中的响应端,我们以类似的方式定义动态部分。唯一的区别是,还存在 byType
匹配器。验证器引擎检查四个字段,以验证来自测试的响应是否具有 JSON 路径与给定字段匹配、与响应正文中定义的类型相同,以及通过以下检查(基于正在调用的方法)的值:
For the response side in the bodyMatchers
section, we define the dynamic parts in a
similar manner. The only difference is that the byType
matchers are also present. The
verifier engine checks four fields to verify whether the response from the test
has a value for which the JSON path matches the given field, is of the same type as the one
defined in the response body, and passes the following check (based on the method being called):
-
For
$.valueWithTypeMatch
, the engine checks whether the type is the same. -
For
$.valueWithMin
, the engine checks the type and asserts whether the size is greater than or equal to the minimum occurrence. -
For
$.valueWithMax
, the engine checks the type and asserts whether the size is smaller than or equal to the maximum occurrence. -
For
$.valueWithMinMax
, the engine checks the type and asserts whether the size is between the minimum and maximum occurrence.
结果的测试类似于以下示例(请注意,and
部分将自动生成的断言与来自匹配器的断言分开):
The resulting test resembles the following example (note that an and
section
separates the autogenerated assertions and the assertion from matchers):
// 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");
请注意,对于 byCommand
方法,示例调用了 assertThatValueIsANumber
。该方法必须在测试基类中定义,或最好静态导入到您的测试中。请注意,byCommand
调用已转换为 assertThatValueIsANumber(parsedJson.read("$.duck"));
。这意味着引擎采用了方法名,并将合适的 JSON 路径作为参数传递给了它。
Notice that, for the byCommand
method, the example calls the
assertThatValueIsANumber
. This method must be defined in the test base class or be
statically imported to your tests. Notice that the byCommand
call was converted to
assertThatValueIsANumber(parsedJson.read("$.duck"));
. That means that the engine took
the method name and passed the proper JSON path as a parameter to it.
结果的 WireMock 存根位于以下示例中:
The resulting WireMock stub is in the following example:
Unresolved directive in dsl-dynamic-properties.adoc - include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[]
如果您使用 matcher
,则 matcher
通过 JSON 路径寻址的请求和响应部分将从断言中移除。在验证集合时,必须为集合的 all 元素创建匹配器。
If you use a matcher
, the part of the request and response that the
matcher
addresses with the JSON Path gets removed from the assertion. In the case of
verifying a collection, you must create matchers for all the elements of the
collection.
请考虑以下示例:
Consider the following example:
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('.+'))
}
}
}
前面的代码导致创建以下测试(代码块仅显示断言部分):
The preceding code leads to creating the following test (the code block shows only the assertion section):
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(…)
方法对其进行断言。
Note that the assertion is malformed. Only the first element of the array got
asserted. To fix this, apply the assertion to the whole $.events
collection and assert it with the byCommand(…)
method.