SpringCloud “匹配器”部分中的动态Properties
如果您使用Pact,那么以下讨论可能看起来很熟悉。很少有用户习惯于在主体之间进行分隔并设置合同的动态部分。
您可以使用bodyMatchers
部分有两个原因:
- 定义应该以存根结尾的动态值。您可以在合同的
request
或inputMessage
部分中进行设置。 - 验证测试结果。本部分位于合同的
response
或outputMessage
中。
当前,Spring Cloud Contract验证程序仅支持具有以下匹配可能性的基于JSON路径的匹配器:
Groovy DSL
对于存根(在消费者方面的测试中):
byEquality()
:通过提供的JSON路径从消费者请求中获取的值必须等于合同中提供的值。byRegex(…)
:通过提供的JSON路径从消费者请求中获取的值必须与正则表达式匹配。您还可以传递期望的匹配值的类型(例如asString()
,asLong()
等)byDate()
:通过提供的JSON路径从消费者请求中获取的值必须与ISO日期值的正则表达式匹配。byTimestamp()
:通过提供的JSON路径从消费者请求中获取的值必须与ISO DateTime值的正则表达式匹配。byTime()
:通过提供的JSON路径从消费者请求中获取的值必须与ISO时间值的正则表达式匹配。
进行验证(在生产者方生成的测试中):
byEquality()
:通过提供的JSON路径从生产者的响应中获取的值必须等于合同中提供的值。byRegex(…)
:通过提供的JSON路径从生产者的响应中获取的值必须与正则表达式匹配。byDate()
:通过提供的JSON路径从生产者的响应中获取的值必须与ISO日期值的正则表达式匹配。byTimestamp()
:通过提供的JSON路径从生产者的响应中获取的值必须与ISO DateTime值的正则表达式匹配。byTime()
:通过提供的JSON路径从生产者的响应中获取的值必须与ISO时间值的正则表达式匹配。byType()
:通过提供的JSON路径从生产者的响应中获取的值必须与合同中的响应主体中定义的类型相同。byType
可以关闭,您可以在其中设置minOccurrence
和maxOccurrence
。对于请求端,应该使用闭包声明集合的大小。这样,您可以声明展平集合的大小。要检查未展平的集合的大小,请对byCommand(…)
testMatcher使用自定义方法。byCommand(…)
:通过提供的JSON路径从生产者的响应中获取的值作为输入传递到您提供的自定义方法。例如,byCommand('foo($it)')
导致调用foo
方法,与JSON路径匹配的值将传递到该方法。从JSON读取的对象的类型可以是以下之一,具体取决于JSON路径:String
:如果您指向String
值。JSONArray
:如果指向List
。Map
:如果指向Map
。Number
:如果指向Integer
,Double
或其他类型的数字。Boolean
:如果指向Boolean
。
byNull()
:通过提供的JSON路径从响应中获取的值必须为null
YAML。 请阅读Groovy部分,详细了解类型的含义
对于YAML,匹配器的结构如下所示
- path: $.foo type: by_regex value: bar 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: $.foo type: by_regex predefined: only_alpha_unicode
在下面,您可以找到允许的“类型”列表。
对于
stubMatchers
:by_equality
by_regex
by_date
by_timestamp
by_time
by_type
- 还有2个其他字段被接受:
minOccurrence
和maxOccurrence
。
- 还有2个其他字段被接受:
对于
testMatchers
:by_equality
by_regex
by_date
by_timestamp
by_time
by_type
- 还有2个其他字段:
minOccurrence
和maxOccurrence
。
- 还有2个其他字段:
by_command
by_null
您还可以通过regexType
字段定义正则表达式对应的类型。在下面,您可以找到允许的正则表达式类型列表:
- as_integer
- as_double
- as_float,
- 只要
- as_short
- as_boolean
- as_string
考虑以下示例:
Groovy DSL。
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: 'bob@email.com' 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
在前面的示例中,您可以在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存根为:
''' { "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" ] } } '''
如果您使用
matcher
,则matcher
用JSON路径寻址的请求和响应部分将从声明中删除。在验证集合的情况下,必须为集合的所有元素创建匹配器。
考虑以下示例:
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(…)
方法进行断言。
更多建议: