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等。您不想强制消费者存根其时钟来始终返回相同的时间值,以便存根与之匹配。 对于Groovy DSL,您可以在契约中通过两种方式提供动态部分:直接将它们传递到正文中或将它们设置在一个称为`bodyMatchers`的单独部分中。

2.0.0 之前,这些是使用 testMatchersstubMatchers 设置的。查看 migration guide 了解更多信息。

对于YAML,您只能使用`matchers`部分。

matchers 中的条目必须引用有效载荷中的现有元素。有关更多信息,请参阅 this issue

Dynamic Properties inside the Body

本节只对编码的 DSL(Groovy、Java 等)有效。请参阅Dynamic Properties in the Matchers Sections 部分,了解类似特性的 YAML 示例。

您可以使用`value`方法在正文内设置属性,或者,如果您使用Groovy映射表示法,则可以使用`$()`设置属性。以下示例显示了如何使用`value`方法设置动态属性:

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

两种方法同样适用。`stub`和`client`方法是`consumer`方法的别名。后面的部分将仔细介绍您可以对这些值执行哪些操作。

Regular Expressions

本节只对 Groovy DSL 有效。请参阅Dynamic Properties in the Matchers Sections 部分,了解类似特性的 YAML 示例。

您可以在契约DSL中使用正则表达式来编写请求。当您想要指定应该为遵循给定模式的请求提供给定的响应时,这样做尤其有用。此外,当您需要对测试和服务器端测试同时使用模式而不用具体值时,可以使用正则表达式。

确保正则表达式符合一个序列的整个区域,因为在内部会调用 Pattern.matches()。例如,abc`不符合`aabc,但`.abc`符合。还有一些其他known limitations

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

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[role=include]
Java
link:{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[role=include]
Kotlin
link:{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[role=include]

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

link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[role=include]

在上一个示例中,通信的另一方为请求和响应生成了各自的数据。

Spring Cloud Contract带有您可以在契约中使用的一系列预定义正则表达式,如下面的示例所示:

link:{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexPatterns.java[role=include]

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

link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[role=include]

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

link:{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexCreatingProperty.java[role=include]

以下示例显示了如何引用这些方法:

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[role=include]
Kotlin
link:{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[role=include]

Limitations

由于生成字符串时`Xeger` 库的某些限制,如果您依赖于自动生成,请勿在正则表达式中使用`$` 和`^` 符号。请参阅 Issue 899

不要将 LocalDate 实例用作 $ 的值(例如 $(consumer(LocalDate.now())))。这会造成 java.lang.StackOverflowError。改用 $(consumer(LocalDate.now().toString()))。请参见 Issue 900

Passing Optional Parameters

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参见 Dynamic Properties in the Matchers Sections 部分。

可以在契约中提供可选参数。但是,你只能为以下内容提供可选参数:

  • 请求的 STUB 端

  • 响应的 TEST 端

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

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[role=include]
Java
link:{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[role=include]
Kotlin
link:{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[role=include]

通过用 optional() 方法包装一部分内容,你创建了一个必须出现 0 次或更多次的正则表达式。

如果你使用 Spock,则会从上一个示例生成以下测试:

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[role=include]

还将生成以下存根:

link:{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[role=include]

Calling Custom Methods on the Server Side

本节只对 Groovy DSL 有效。请参阅Dynamic Properties in the Matchers Sections 部分,了解类似特性的 YAML 示例。

可以在测试期间在服务器端运行定义一个方法调用。可以在配置中将此方法添加到定义为 baseClassForTests 的类中。以下代码显示了测试用例的契约部分示例:

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[role=include]
Java
link:{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[role=include]
Kotlin
link:{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[role=include]

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

link:{plugins_path}/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/org/springframework/cloud/contract/verifier/twitter/places/BaseMockMvcSpec.groovy[role=include]

您无法同时使用 Stringexecute 来执行连接。例如,调用 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 中读取一个对象:

link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy[role=include]

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

// 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

最好的情况是提供固定值,但有时候你需要在响应中引用一个请求。

如果你在 Groovy DSL 中编写契约,则可以使用 fromRequest() 方法,该方法允许你从 HTTP 请求中引用一堆元素。可以使用以下选项:

  • 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 Contract 函数来实现此目的。在这种情况下,您可以使用以下选项:

  • {{{ 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' }}}

考虑以下契约:

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[role=include]
YAML
link:{verifier_root_path}/src/test/resources/yml/contract_reference_request.yml[role=include]
Java
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());
				});
			});
		});
	}

}
Kotlin
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 测试生成会生成类似于以下示例的测试:

// 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 Contract Verifier 使用 WireMock 的 response-template 响应转换器。它使用 Handlebars 将 Mustache {{{ }}} 模板转换为合适的值。此外,它注册了两个帮助函数:

  • escapejsonbody:以可嵌入 JSON 的格式对请求正文进行转义。

  • jsonpath:对于给定的参数,查找请求正文中的对象。

Dynamic Properties in the Matchers Sections

如果您使用 Pact,以下讨论可能看起来很熟悉。很多用户习惯于将合同的动态部分与主体分开设置。

你可以出于两个原因使用 bodyMatchers 部分:

  • 定义应该成为存根中的动态值。可以在契约的 request 部分进行设置。

  • 验证测试结果。此部分存在于契约的 responseoutputMessage 端。

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

Coded DSL

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

  • byEquality():在提供的 JSON 路径中从使用者请求中获取的值必须等于契约中提供的数值。

  • byRegex(&#8230;&#8203;):在提供的 JSON 路径中从使用者请求中获取的值必须匹配正则表达式。您还可以传递预期匹配值类型(例如 asString()asLong() 等)。

  • byDate():在提供的 JSON 路径中从使用者请求中获取的值必须匹配 ISO Date 值的正则表达式。

  • byTimestamp():从消费者请求中采用提供的 JSON 路径中的值的正则表达式必须与 ISO 日期时间值匹配。

  • byTime():从消费者请求中采用提供的 JSON 路径中的值的正则表达式必须与 ISO 时间值匹配。

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

  • byEquality():从生产者的响应中采用提供的 JSON 路径中的值必须等于合同中提供的值。

  • byRegex(&#8230;&#8203;):从生产者的响应中采用提供的 JSON 路径中的值必须与正则表达式匹配。

  • byDate():从生产者的响应中采用提供的 JSON 路径中的值必须与 ISO 日期值的正则表达式匹配。

  • byTimestamp():从生产者的响应中采用提供的 JSON 路径中的值必须与 ISO 日期时间值的正则表达式匹配。

  • byTime():从生产者的响应中采用提供的 JSON 路径中的值必须与 ISO 时间值的正则表达式匹配。

  • byType():从生产者的响应中采用提供的 JSON 路径中的值需要与合同中响应体中定义的类型相同。byType 可以采用一个闭包,其中可以设置 minOccurrencemaxOccurrence。对于请求方,应使用闭包断言集合大小。这样,可以断言扁平化集合的大小。要检查非扁平化集合的大小,请使用自定义方法及 byCommand(&#8230;&#8203;) testMatcher

  • byCommand(&#8230;&#8203;):从生产者的响应中采用提供的 JSON 路径中的值作为为用户提供的自定义方法的输入。例如,byCommand('thing($it)') 结果为调用 thing 方法,其中与 JSON 路径匹配的值会传递给该方法。从 JSON 中读取对象的类型可以是以下之一,具体取决于 JSON 路径:

    • String:如果指向 String 值。

    • JSONArray:如果指向 List

    • Map:如果指向 Map

    • Number:如果指向 IntegerDouble 或其他类型的数字。

    • Boolean:如果指向 Boolean

  • byNull():从响应中采用提供的 JSON 路径中的值必须为 null。

YAML

请参阅 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 值列表:

  • For stubMatchers:

    • by_equality

    • by_regex

    • by_date

    • by_timestamp

    • by_time

    • by_type

      • 接受两个附加字段(minOccurrencemaxOccurrence)。

  • For 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

请考虑以下示例:

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy[role=include]
YAML
link:{verifier_root_path}/src/test/resources/yml/contract_matchers.yml[role=include]

在前一个示例中,你可以在 matchers 部分看到合同的动态部分。对于请求部分,你可以看到,对于所有字段而非 valueWithoutAMatcher,存根应当包含的正则表达式的值被明确设置。对于 valueWithoutAMatcher,验证像不使用匹配器一样进行。在这种情况下,测试执行相等性检查。

对于 bodyMatchers 部分中的响应端,我们以类似的方式定义动态部分。唯一的区别是,还存在 byType 匹配器。验证器引擎检查四个字段,以验证来自测试的响应是否具有 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\",\"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 路径作为参数传递给了它。

结果的 WireMock 存根位于以下示例中:

link:{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[role=include]

如果您使用 matcher,则 matcher 通过 JSON 路径寻址的请求和响应部分将从断言中移除。在验证集合时,必须为集合的 all 元素创建匹配器。

请考虑以下示例:

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(…​) 方法对其进行断言。