动态性质

契约可以包含一些动态属性:时间戳、ID等等。你不会 想强制消费者设置时钟,才能始终返回相同的时间值 这样它才能被存根匹配。spring-doc.cadn.net.cn

对于Groovy DSL,你可以在合同中提供动态部分 有两种方式:直接传入正体,或放在一个称为bodyMatchers.spring-doc.cadn.net.cn

在2.0.0之前,这些设置是通过使用测试匹配者stubMatchers. 更多信息请参见迁移指南

对于 YAML,你只能使用匹配器部分。spring-doc.cadn.net.cn

内部条目匹配器必须引用有效载荷的现有元素。更多信息请参见本期。

体内的动态属性

本节仅适用于编码DSL(如Groovy、Java等)。有关类似功能的 YAML 示例,请参见匹配器部分的动态属性

你可以在主体内设置属性,比如方法或,如果你使用 Groovy 映射符号,其 。以下示例展示了如何设置动态 用价值方法的性质:$()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(...))

两种方法同样有效。这存根客户端方法是消费者方法。后续章节将更深入探讨你能如何利用这些数值。spring-doc.cadn.net.cn

正则表达式

本节仅适用于Groovy DSL。有关类似功能的YAML示例,请参见匹配器部分的动态属性

你可以用正则表达式在契约 DSL 中写入请求。这样做是 尤其适用于你想表明应提供某个回答时 对于遵循特定模式的请求。另外,你也可以用正则表达式 无论是测试还是服务器端测试,都需要使用模式而非精确数值。spring-doc.cadn.net.cn

确保正则表达式匹配序列的整个区域,因为内部,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")
		}
	}
}

在前述例子中,通信的对面拥有相应的数据 为请求和回复而生成。spring-doc.cadn.net.cn

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

在你的合同中,你可以这样使用(以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]'))}]"
		)
	}
}

为了让事情更简单,你可以使用一组预定义的对象,这些对象会自动假设你想传递一个正则表达式。所有这些方法都以任何前缀如下: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")
        }
    }
}

局限性

由于某些限制泽格尔生成字符串的库正则表达式,如果你依赖自动表达式,请不要在正则表达式中使用 and 符号 代。 参见第899期$^
不要使用本地日期实例作为值(例如,$$(consumer(LocalDate.now())). 它会导致java.lang.StackOverflowError(java.lang.StackOverflowError). 用$(consumer(LocalDate.now().toString()))相反。 参见第900期

传递可选参数

本节仅适用于Groovy DSL。有关类似功能的YAML示例,请参见匹配器部分的动态属性

你可以在合同中提供可选参数。但是,你只能为以下情况提供可选参数: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")))
        ))
    }
}

通过包裹身体的一部分可选()方法,你创建一个正则表达式,必须出现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}

}

还会生成以下小作品: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
}
'''

调用服务器端的自定义方法

本节仅适用于Groovy DSL。有关类似功能的YAML示例,请参见匹配器部分的动态属性

你可以定义一个在测试期间在服务器端运行的方法调用。这样的方法可以添加到定义为baseClassForTests在配置中。 这 以下代码展示了测试用例中合同部分的示例:spring-doc.cadn.net.cn

槽的
method GET()
Java
r.method(r.GET());
Kotlin
method = GET

以下代码展示了测试用例的基类部分: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
	}

}
你不能同时使用a。字符串执行执行串接。 为 示例,呼叫header('Authorization', 'Bearer '+ execute('authToken()')导致不当结果。相反,叫header('Authorization', execute('authToken()'))和 确保authToken()方法返回你需要的所有内容。

从JSON读取的对象类型可以是以下之一,具体取决于JSON路径:spring-doc.cadn.net.cn

在合同的请求部分,你可以指定身体应从一种方法中取。spring-doc.cadn.net.cn

你必须同时提供消费者和生产者双方。 这执行部分 适用于全身,而非部分部位。

以下示例展示了如何从JSON读取对象:spring-doc.cadn.net.cn

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

上述示例导致将hashCode()请求体中的方法。它应类似于以下代码: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);

引用响应中的请求

最好的做法是提供固定值,但有时你需要在回复中引用一个请求。spring-doc.cadn.net.cn

如果你在Groovy DSL里写合同,可以用fromRequest()方法,允许你引用HTTP请求中的大量元素。你可以使用以下方法 选项:spring-doc.cadn.net.cn

如果你使用YAML合同定义或Java定义,你必须使用Handlebars符号和自定义的Spring Cloud Contract函数。实现这一点。在这种情况下,你可以使用以下选项:{{{ }}}spring-doc.cadn.net.cn

  • {{{ 请求.url }}}返回请求URL和查询参数。spring-doc.cadn.net.cn

  • {{{ request.query.key。[索引] }}}返回第n个查询参数,并返回给定名称。例如,对于东西,第一个条目为{{{ request.query.thing.[0] }}}spring-doc.cadn.net.cn

  • {{{ 请求.路径 }}}返回完整路径。spring-doc.cadn.net.cn

  • {{{ 请求.path。[索引] }}}返回第n条路径元素。例如 第一个条目是{{{ request.path.[0] }}}`spring-doc.cadn.net.cn

  • {{{ request.headers.key }}}: 返回第一个带有名字的头部。spring-doc.cadn.net.cn

  • {{{ request.headers.key。[索引] }}}: 返回带有名字的第n个头。spring-doc.cadn.net.cn

  • {{{ 请求.body }}}返回完整的请求正文。spring-doc.cadn.net.cn

  • {{{ jsonpath this 'your.json.path' }}}返回请求中的元素 与JSON路径相符。例如,对于 的 JSON 路径$。这里{{{ jsonpath this '$.here' }}}spring-doc.cadn.net.cn

请考虑以下合同:spring-doc.cadn.net.cn

槽的
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)
        }
    }
}

运行JUnit测试生成后,会得到类似以下示例的测试: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");

如你所见,请求中的元素在回复中已被正确引用。spring-doc.cadn.net.cn

生成的 WireMock 存根应类似于以下示例: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" ]
  }
}

发送如请求合同的一部分结果 发送以下回复正文: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"
}
此功能仅适用于大于或等的WireMock版本 变为2.5.1。Spring Cloud 合约验证器使用 WireMock 的响应模板响应转换器。它用Handlebars把胡子模板转换成 正确的价值观。此外,它还注册了两个辅助功能:{{{ }}}

匹配器部分中的动态属性

如果你和Pact合作,以下讨论可能听起来很熟悉。 相当多的用户习惯于身体和设置之间保持分离 合同中的动态部分。spring-doc.cadn.net.cn

你可以使用bodyMatchers有两个原因:spring-doc.cadn.net.cn

  • 定义动态值,最终应该出现在存根中。 你可以把它设置在请求这是你合同的一部分。spring-doc.cadn.net.cn

  • 核实你的测试结果。 该部分包含在响应输出消息一侧 合同。spring-doc.cadn.net.cn

目前,Spring Cloud 合同验证器仅支持基于 JSON 路径的匹配器,且 匹配可能性如下:spring-doc.cadn.net.cn

编码DSL

关于存根(在消费者端测试中):spring-doc.cadn.net.cn

  • byEquality(): 从消费者请求中获得的 JSON 路径中的值必须为 等于合同中规定的价值。spring-doc.cadn.net.cn

  • byRegex(...): 从消费者请求中获取的 JSON 路径中的值必须 匹配正则表达式。你也可以传递期望匹配值的类型(例如,asString(),asLong(),依此类推)。spring-doc.cadn.net.cn

  • byDate(): 从消费者请求中获取的 JSON 路径中的值必须 匹配正则表达式以获得ISO日期值。spring-doc.cadn.net.cn

  • byTimestamp(): 从消费者请求中获取的 JSON 路径中的值必须 匹配正则表达式以获得 ISO DateTime 值。spring-doc.cadn.net.cn

  • byTime(): 从消费者请求中获取的 JSON 路径中的值必须 匹配正则表达式以获得 ISO 时间值。spring-doc.cadn.net.cn

验证时(生产方生成的测试):spring-doc.cadn.net.cn

  • byEquality():从生产者响应中获取的JSON路径中的值必须为 等于合同中规定的价值。spring-doc.cadn.net.cn

  • byRegex(...): 从提供JSON路径中生产者响应中取的值必须 匹配正则表达式。spring-doc.cadn.net.cn

  • byDate(): 从生产者响应中获得的 JSON 路径中的值必须相符 ISO日期值的正则表达式。spring-doc.cadn.net.cn

  • byTimestamp(): 从提供JSON路径中生产者响应中取的值必须 匹配正则表达式以获得 ISO DateTime 值。spring-doc.cadn.net.cn

  • byTime(): 从生产者响应中获得的 JSON 路径中的值必须相符 ISO时间值的正则表达式。spring-doc.cadn.net.cn

  • byType(): 从生产者响应中获取的 JSON路径值为 与合同中答复正文中定义的类型相同。byType可以取闭包,你可以设最小发生maxOccurrence.对于 请求端,你应该用闭合来断言集合的大小。 这样,你就可以断言扁平化集合的大小。检查 未展开的集合,使用自定义方法,配合byCommand(...) 测试匹配器.spring-doc.cadn.net.cn

  • byCommand(...):从生产者响应中获得的JSON路径中的值为 作为你提供的自定义方法的输入传递。例如byCommand('thing($it)')结果为东西该方法的值与 JSON Path 会被通过。从JSON读取的对象类型可以是 以下内容取决于 JSON 路径:spring-doc.cadn.net.cn

  • byNull():从所给JSON路径中响应取的值必须为空。spring-doc.cadn.net.cn

YAML

详见Groovy部分的详细说明 类型意味着什么。

对于YAML,匹配器的结构类似于以下示例:spring-doc.cadn.net.cn

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

或者,如果你想使用预定义的正则表达式之一[only_alpha_unicode,编号,any_boolean,ip_address,宿主名, 电子邮件、网址、UUID、iso_date、iso_date_time、iso_time、iso_8601_with_offset、non_empty, non_blank]你可以用类似下面的例子:spring-doc.cadn.net.cn

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

以下列表显示了允许的列表类型值:spring-doc.cadn.net.cn

你也可以在正规表达式类型田。以下列表显示了允许的正则表达式类型:spring-doc.cadn.net.cn

请考虑以下例子:spring-doc.cadn.net.cn

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

在前面的例子中,你可以看到合同的动态部分匹配器部分。关于请求部分,你可以看到所有字段都有,但价值无AMatcher,存根应有的正则表达式值 contains 是显式设置的。为价值无AMatcher,验证进行 就像没有使用匹配器一样。在这种情况下,检验执行 平等检查。spring-doc.cadn.net.cn

对于响应方来说bodyMatchers在节中,我们定义了动态部分 类似的方式。唯一的区别是byType匹配器也存在。这 验证器引擎检查四个字段以验证测试响应是否 有一个值,使得JSON路径与给定字段匹配,且与该字段类型相同 定义在响应体中,并通过以下检查(基于调用的方法):spring-doc.cadn.net.cn

  • $.valueWithTypeMatch(类型匹配)发动机检查类型是否相同。spring-doc.cadn.net.cn

  • $.valueWithMin发动机检查类型并断言尺寸是否更大 大于或等于最小发生率。spring-doc.cadn.net.cn

  • $.value与Max一起发动机检查类型并断言尺寸是否为 小于或等于最大发生率。spring-doc.cadn.net.cn

  • $.valueWithMinMax发动机检查类型并断言尺寸是否为 介于最小发生率和最大发生率之间。spring-doc.cadn.net.cn

所得测试类似于以下示例(注意部分 将自动生成的断言和断言与匹配者分离):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");
注意,对于byCommand(指挥部)该示例称断言那个价值是个数字.该方法必须在测试基类中定义,或为 静态导入到你的测试中。注意byCommand(指挥部)呼号改为assertThatValueIsANumber(parsedJson.read(“$.duck”));.这意味着发动机发生了 方法是方法名称,并将正确的 JSON 路径作为参数传递给它。

由此产生的 WireMock 存根如下示例: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" ]
  }
}
'''
如果你使用匹配器,请求和响应中匹配器带有 JSON 路径的地址会从断言中移除。在 验证集合时,你必须为所有元素创建匹配器 收集。

请考虑以下例子: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('.+'))
        }
    }
}

上述代码引导我创建以下测试(代码块仅显示断言部分):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(".+")

注意,该断言形式不正确。只有阵列的第一个元素得到了 断言。要解决这个问题,将断言应用于整体$.事件收集并用byCommand(...)方法。spring-doc.cadn.net.cn