此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 spring-cloud-contract 4.2.0spring-doc.cadn.net.cn

动态属性

合同可以包含一些动态属性:时间戳、ID 等。你不需要 想要强制 Consumer 对他们的 clockstub 进行存根,以始终返回相同的 time 值 以便它与存根匹配。spring-doc.cadn.net.cn

对于 Groovy DSL,您可以在 Contract 中提供动态部分 有两种方式:直接在正文中传递它们,或者将它们设置在名为bodyMatchers.spring-doc.cadn.net.cn

在 2.0.0 之前,这些是通过使用testMatchersstubMatchers. 有关更多信息,请参阅迁移指南

对于 YAML,您只能使用matchers部分。spring-doc.cadn.net.cn

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

Body 内的 Dynamic Properties

此部分仅对编码的 DSL(Groovy、Java 等)有效。有关类似功能的 YAML 示例,请参阅 Matchers Sections 部分中的 Dynamic Properties

您可以使用value方法,或者如果您使用 Groovy 映射表示法,其中 .以下示例演示如何设置 dynamic properties 替换为 value 方法:$()spring-doc.cadn.net.cn

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

这两种方法同样有效。这stubclientmethods 是consumer方法。后续部分将仔细研究您可以对这些值执行哪些作。spring-doc.cadn.net.cn

正则表达式

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

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

确保 regex 匹配序列的整个区域,因为在内部,Pattern.matches()被调用。例如abc不匹配aabc.abc确实。 还有一些其他已知限制spring-doc.cadn.net.cn

以下示例演示如何使用正则表达式编写请求:spring-doc.cadn.net.cn

槽的
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'
		}
	}
}
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		r.method("GET");
		r.url(r.$(r.consumer(r.regex("\\/[0-9]{2}")), r.producer("/12")));
	});
	c.response(r -> {
		r.status(r.OK());
		r.body(ContractVerifierUtil.map()
			.entry("id", r.$(r.anyNumber()))
			.entry("surname", r.$(r.consumer("Kowalsky"), r.producer(r.regex("[a-zA-Z]+")))));
		r.headers(h -> {
			h.header("Content-Type", "text/plain");
		});
	});
});
Kotlin
contract {
    request {
        method = method("GET")
        url = url(v(consumer(regex("\\/[0-9]{2}")), producer("/12")))
    }
    response {
        status = OK
        body(mapOf(
                "id" to v(anyNumber),
                "surname" to v(consumer("Kowalsky"), producer(regex("[a-zA-Z]+")))
        ))
        headers {
            header("Content-Type", "text/plain")
        }
    }
}

您也可以使用正则表达式仅提供通信的一侧。如果你 执行此作,则合约引擎会自动提供生成的匹配 提供的正则表达式。以下代码显示了 Groovy 的示例:spring-doc.cadn.net.cn

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

在前面的示例中,通信的另一端具有相应的数据 为 request 和 response 生成。spring-doc.cadn.net.cn

Spring Cloud Contract 附带了一系列预定义的正则表达式,您可以 在你的 Contract 中使用,如下例所示:spring-doc.cadn.net.cn

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();
}

在你的 Contract 中,你可以按如下方式使用它(Groovy DSL 的示例):spring-doc.cadn.net.cn

Contract dslWithOptionalsInString = Contract.make {
	priority 1
	request {
		method POST()
		url '/users/password'
		headers {
			contentType(applicationJson())
		}
		body(
				email: $(consumer(optional(regex(email()))), producer('[email protected]')),
				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('[email protected]'))}]"
		)
	}
}

为了让事情变得更简单,您可以使用一组预定义的对象,这些对象会自动 假设您希望传递正则表达式。 所有这些方法都以any前缀,如下所示:spring-doc.cadn.net.cn

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);

下面的示例演示如何引用这些方法:spring-doc.cadn.net.cn

槽的
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'))
		])
	}
}
Kotlin
contract {
    name = "foo"
    label = "trigger_event"
    input {
        triggeredBy = "toString()"
    }
    outputMessage {
        sentTo = sentTo("topic.rateablequote")
        body(mapOf(
                "alpha" to v(anyAlphaUnicode),
                "number" to v(anyNumber),
                "anInteger" to v(anyInteger),
                "positiveInt" to v(anyPositiveInt),
                "aDouble" to v(anyDouble),
                "aBoolean" to v(aBoolean),
                "ip" to v(anyIpAddress),
                "hostname" to v(anyAlphaUnicode),
                "email" to v(anyEmail),
                "url" to v(anyUrl),
                "httpsUrl" to v(anyHttpsUrl),
                "uuid" to v(anyUuid),
                "date" to v(anyDate),
                "dateTime" to v(anyDateTime),
                "time" to v(anyTime),
                "iso8601WithOffset" to v(anyIso8601WithOffset),
                "nonBlankString" to v(anyNonBlankString),
                "nonEmptyString" to v(anyNonEmptyString),
                "anyOf" to v(anyOf('foo', 'bar'))
        ))
        headers {
            header("Content-Type", "text/plain")
        }
    }
}

局限性

由于Xeger库生成一个字符串 一个正则表达式,如果您依赖 Automatic,请不要在正则表达式中使用 和 符号 代。参见 问题 899$^
请勿使用LocalDateinstance 的值(例如$$(consumer(LocalDate.now()))). 它会导致java.lang.StackOverflowError.用$(consumer(LocalDate.now().toString()))相反。 参见 问题 900

传递可选参数

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

您可以在合同中提供可选参数。但是,您可以提供 可选参数仅适用于以下各项:spring-doc.cadn.net.cn

以下示例说明如何提供可选参数:spring-doc.cadn.net.cn

槽的
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('[email protected]')),
				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")))
		)
	}
}
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.priority(1);
	c.name("optionals");
	c.request(r -> {
		r.method("POST");
		r.url("/users/password");
		r.headers(h -> {
			h.contentType(h.applicationJson());
		});
		r.body(ContractVerifierUtil.map()
			.entry("email", r.$(r.consumer(r.optional(r.regex(r.email()))), r.producer("[email protected]")))
			.entry("callback_url",
					r.$(r.consumer(r.regex(r.hostname())), r.producer("https://partners.com"))));
	});
	c.response(r -> {
		r.status(404);
		r.headers(h -> {
			h.header("Content-Type", "application/json");
		});
		r.body(ContractVerifierUtil.map()
			.entry("code", r.value(r.consumer("123123"), r.producer(r.optional("123123")))));
	});
});
Kotlin
contract { c ->
    priority = 1
    name = "optionals"
    request {
        method = POST
        url = url("/users/password")
        headers {
            contentType = APPLICATION_JSON
        }
        body = body(mapOf(
                "email" to v(consumer(optional(regex(email))), producer("[email protected]")),
                "callback_url" to v(consumer(regex(hostname)), producer("https://partners.com"))
        ))
    }
    response {
        status = NOT_FOUND
        headers {
            header("Content-Type", "application/json")
        }
        body(mapOf(
                "code" to value(consumer("123123"), producer(optional("123123")))
        ))
    }
}

通过将身体的一部分包裹起来optional()方法,您可以创建一个常规的 expression 的表达式,该表达式必须出现 0 次或多次。spring-doc.cadn.net.cn

如果您使用 Spock,则将根据前面的示例生成以下测试:spring-doc.cadn.net.cn

槽的
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.*

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":"[email protected]","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:spring-doc.cadn.net.cn

					'''
{
  "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 == [[email protected]]\\"}",
	"headers" : {
	  "Content-Type" : "application/json"
	}
  },
  "priority" : 1
}
'''

Calling Custom Methods on the Server Side

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.

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:spring-doc.cadn.net.cn

Groovy
method GET()
Java
r.method(r.GET());
Kotlin
method = GET

The following code shows the base class portion of the test case:spring-doc.cadn.net.cn

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
	}

}
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.

The type of the object read from the JSON can be one of the following, depending on the JSON path:spring-doc.cadn.net.cn

In the request part of the contract, you can specify that the body should be taken from a method.spring-doc.cadn.net.cn

You must provide both the consumer and the producer side. The execute part is applied for the whole body, not for parts of it.

The following example shows how to read an object from JSON:spring-doc.cadn.net.cn

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

The preceding example results in calling the hashCode() method in the request body. It should resemble the following code:spring-doc.cadn.net.cn

// 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.spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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:{{{ }}}spring-doc.cadn.net.cn

  • {{{ request.url }}}: Returns the request URL and query parameters.spring-doc.cadn.net.cn

  • {{{ request.query.key.[index] }}}: Returns the nth query parameter with the given name. For example, for a key of thing, the first entry is {{{ request.query.thing.[0] }}}spring-doc.cadn.net.cn

  • {{{ request.path }}}: Returns the full path.spring-doc.cadn.net.cn

  • {{{ request.path.[index] }}}: Returns the nth path element. For example, the first entry is {{{ request.path.[0] }}}`spring-doc.cadn.net.cn

  • {{{ request.headers.key }}}: Returns the first header with the given name.spring-doc.cadn.net.cn

  • {{{ request.headers.key.[index] }}}: Returns the nth header with the given name.spring-doc.cadn.net.cn

  • {{{ request.body }}}: Returns the full request body.spring-doc.cadn.net.cn

  • {{{ 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' }}}spring-doc.cadn.net.cn

Consider the following contract:spring-doc.cadn.net.cn

Groovy
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())
	}
}
YAML
request:
  method: GET
  url: /api/v1/xxxx
  queryParameters:
    foo:
      - bar
      - bar2
  headers:
    Authorization:
      - secret
      - secret2
  body:
    foo: bar
    baz: 5
response:
  status: 200
  headers:
    Authorization: "foo {{{ request.headers.Authorization.0 }}} bar"
  body:
    url: "{{{ request.url }}}"
    path: "{{{ request.path }}}"
    pathIndex: "{{{ request.path.1 }}}"
    param: "{{{ request.query.foo }}}"
    paramIndex: "{{{ request.query.foo.1 }}}"
    authorization: "{{{ request.headers.Authorization.0 }}}"
    authorization2: "{{{ request.headers.Authorization.1 }}"
    fullBody: "{{{ request.body }}}"
    responseFoo: "{{{ jsonpath this '$.foo' }}}"
    responseBaz: "{{{ jsonpath this '$.baz' }}}"
    responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla"
Java
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
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)
        }
    }
}

Running a JUnit test generation leads to a test that resembles the following example:spring-doc.cadn.net.cn

// 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.spring-doc.cadn.net.cn

The generated WireMock stub should resemble the following example:spring-doc.cadn.net.cn

{
  "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" ]
  }
}

Sending a request such as the one presented in the request part of the contract results in sending the following response body:spring-doc.cadn.net.cn

{
  "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"
}
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: {{{ }}}

Dynamic Properties in the Matchers Sections

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.spring-doc.cadn.net.cn

You can use the bodyMatchers section for two reasons:spring-doc.cadn.net.cn

  • Define the dynamic values that should end up in a stub. You can set it in the request part of your contract.spring-doc.cadn.net.cn

  • Verify the result of your test. This section is present in the response or outputMessage side of the contract.spring-doc.cadn.net.cn

Currently, Spring Cloud Contract Verifier supports only JSON path-based matchers with the following matching possibilities:spring-doc.cadn.net.cn

Coded DSL

For the stubs (in tests on the consumer’s side):spring-doc.cadn.net.cn

  • byEquality(): The value taken from the consumer’s request in the provided JSON path must be equal to the value provided in the contract.spring-doc.cadn.net.cn

  • 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).spring-doc.cadn.net.cn

  • byDate(): The value taken from the consumer’s request in the provided JSON path must match the regex for an ISO Date value.spring-doc.cadn.net.cn

  • byTimestamp(): The value taken from the consumer’s request in the provided JSON path must match the regex for an ISO DateTime value.spring-doc.cadn.net.cn

  • byTime(): The value taken from the consumer’s request in the provided JSON path must match the regex for an ISO Time value.spring-doc.cadn.net.cn

For the verification (in generated tests on the Producer’s side):spring-doc.cadn.net.cn

  • byEquality(): The value taken from the producer’s response in the provided JSON path must be equal to the provided value in the contract.spring-doc.cadn.net.cn

  • byRegex(…​): The value taken from the producer’s response in the provided JSON path must match the regex.spring-doc.cadn.net.cn

  • byDate(): The value taken from the producer’s response in the provided JSON path must match the regex for an ISO Date value.spring-doc.cadn.net.cn

  • byTimestamp(): The value taken from the producer’s response in the provided JSON path must match the regex for an ISO DateTime value.spring-doc.cadn.net.cn

  • byTime(): The value taken from the producer’s response in the provided JSON path must match the regex for an ISO Time value.spring-doc.cadn.net.cn

  • 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 set minOccurrence and maxOccurrence. 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 the byCommand(…​) testMatcher.spring-doc.cadn.net.cn

  • 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 a thing 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:spring-doc.cadn.net.cn

  • byNull(): The value taken from the response in the provided JSON path must be null.spring-doc.cadn.net.cn

YAML

See the Groovy section for a detailed explanation of what the types mean.

For YAML, the structure of a matcher resembles the following example:spring-doc.cadn.net.cn

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

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:spring-doc.cadn.net.cn

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

The following list shows the allowed list of type values:spring-doc.cadn.net.cn

You can also define which type the regular expression corresponds to in the regexType field. The following list shows the allowed regular expression types:spring-doc.cadn.net.cn

Consider the following example:spring-doc.cadn.net.cn

Groovy
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}'))))
		}
	}
}
YAML
request:
  method: GET
  urlPath: /get/1
  headers:
    Content-Type: application/json
  cookies:
    foo: 2
    bar: 3
  queryParameters:
    limit: 10
    offset: 20
    filter: 'email'
    sort: name
    search: 55
    age: 99
    name: John.Doe
    email: '[email protected]'
  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'
    nullValue: null
    valueWithMin:
      - 1
      - 2
      - 3
    valueWithMax:
      - 1
      - 2
      - 3
    valueWithMinMax:
      - 1
      - 2
      - 3
    valueWithMinEmpty: []
    valueWithMaxEmpty: []
  matchers:
    url:
      regex: /get/[0-9]
      # predefined:
      # execute a method
      #command: 'equals($it)'
    queryParameters:
      - key: limit
        type: equal_to
        value: 20
      - key: offset
        type: containing
        value: 20
      - key: sort
        type: equal_to
        value: name
      - key: search
        type: not_matching
        value: '^[0-9]{2}$'
      - key: age
        type: not_matching
        value: '^\\w*$'
      - key: name
        type: matching
        value: 'John.*'
      - key: hello
        type: absent
    cookies:
      - key: foo
        regex: '[0-9]'
      - key: bar
        command: 'equals($it)'
    headers:
      - key: Content-Type
        regex: "application/json.*"
    body:
      - path: $.duck
        type: by_regex
        value: "[0-9]{3}"
      - path: $.duck
        type: by_equality
      - path: $.alpha
        type: by_regex
        predefined: only_alpha_unicode
      - path: $.alpha
        type: by_equality
      - path: $.number
        type: by_regex
        predefined: number
      - path: $.aBoolean
        type: by_regex
        predefined: any_boolean
      - path: $.date
        type: by_date
      - path: $.dateTime
        type: by_timestamp
      - path: $.time
        type: by_time
      - path: "$.['key'].['complex.key']"
        type: by_equality
      - path: $.nullvalue
        type: by_null
      - path: $.valueWithMin
        type: by_type
        minOccurrence: 1
      - path: $.valueWithMax
        type: by_type
        maxOccurrence: 3
      - path: $.valueWithMinMax
        type: by_type
        minOccurrence: 1
        maxOccurrence: 3
response:
  status: 200
  cookies:
    foo: 1
    bar: 2
  body:
    duck: 123
    alpha: "abc"
    number: 123
    aBoolean: true
    date: "2017-01-01"
    dateTime: "2017-01-01T01:23:45"
    time: "01:02:34"
    valueWithoutAMatcher: "foo"
    valueWithTypeMatch: "string"
    valueWithMin:
      - 1
      - 2
      - 3
    valueWithMax:
      - 1
      - 2
      - 3
    valueWithMinMax:
      - 1
      - 2
      - 3
    valueWithMinEmpty: []
    valueWithMaxEmpty: []
    key:
      'complex.key': 'foo'
    nulValue: null
  matchers:
    headers:
      - key: Content-Type
        regex: "application/json.*"
    cookies:
      - key: foo
        regex: '[0-9]'
      - key: bar
        command: 'equals($it)'
    body:
      - path: $.duck
        type: by_regex
        value: "[0-9]{3}"
      - path: $.duck
        type: by_equality
      - path: $.alpha
        type: by_regex
        predefined: only_alpha_unicode
      - path: $.alpha
        type: by_equality
      - path: $.number
        type: by_regex
        predefined: number
      - path: $.aBoolean
        type: by_regex
        predefined: any_boolean
      - path: $.date
        type: by_date
      - path: $.dateTime
        type: by_timestamp
      - path: $.time
        type: by_time
      - path: $.valueWithTypeMatch
        type: by_type
      - path: $.valueWithMin
        type: by_type
        minOccurrence: 1
      - path: $.valueWithMax
        type: by_type
        maxOccurrence: 3
      - path: $.valueWithMinMax
        type: by_type
        minOccurrence: 1
        maxOccurrence: 3
      - path: $.valueWithMinEmpty
        type: by_type
        minOccurrence: 0
      - path: $.valueWithMaxEmpty
        type: by_type
        maxOccurrence: 0
      - path: $.duck
        type: by_command
        value: assertThatValueIsANumber($it)
      - path: $.nullValue
        type: by_null
        value: null
  headers:
    Content-Type: application/json

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.spring-doc.cadn.net.cn

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):spring-doc.cadn.net.cn

  • For $.valueWithTypeMatch, the engine checks whether the type is the same.spring-doc.cadn.net.cn

  • For $.valueWithMin, the engine checks the type and asserts whether the size is greater than or equal to the minimum occurrence.spring-doc.cadn.net.cn

  • For $.valueWithMax, the engine checks the type and asserts whether the size is smaller than or equal to the maximum occurrence.spring-doc.cadn.net.cn

  • For $.valueWithMinMax, the engine checks the type and asserts whether the size is between the minimum and maximum occurrence.spring-doc.cadn.net.cn

The resulting test resembles the following example (note that an and section separates the autogenerated assertions and the assertion from matchers):spring-doc.cadn.net.cn

// 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");
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.

The resulting WireMock stub is in the following example:spring-doc.cadn.net.cn

					'''
{
  "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" ]
  }
}
'''
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:spring-doc.cadn.net.cn

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):spring-doc.cadn.net.cn

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(".+")

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.spring-doc.cadn.net.cn