Dynamic properties

契约可以包含一些动态属性:时间戳、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
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
Java
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[]
Kotlin
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]

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

Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[]

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

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

Unresolved directive in dsl-dynamic-properties.adoc - include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexPatterns.java[]

在您的契约中,您可以按如下方式使用它(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`前缀开头,如下所示:

Unresolved directive in dsl-dynamic-properties.adoc - include::{contract_spec_path}/src/main/java/org/springframework/cloud/contract/spec/internal/RegexCreatingProperty.java[]

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

Groovy
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[]
Kotlin
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]

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
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
Java
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[]
Kotlin
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]

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

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

Groovy
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::{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 示例。

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

Groovy
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ContractHttpDocsSpec.groovy[]
Java
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/contractsToCompile/contract_docs_examples.java[]
Kotlin
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/kotlin/contract_docs_examples.kts[]

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

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[]

您无法同时使用 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 中读取一个对象:

Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy[]

前面的示例会导致在请求正文中调用 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
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[]
YAML
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/yml/contract_reference_request.yml[]
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
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy[]
YAML
Unresolved directive in dsl-dynamic-properties.adoc - include::{verifier_root_path}/src/test/resources/yml/contract_matchers.yml[]

在前一个示例中,你可以在 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 存根位于以下示例中:

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 元素创建匹配器。

请考虑以下示例:

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