Support for Pact v3 (#569)
* Upgraded pact-jvm-model to 3.5.13 * Enhanced the conversion of Spring Cloud contracts to Pact contracts using the v3 spec. Introduces breaking change returning a list of `Pact`s instead of one. * Enhanced the conversion of Pact contracts using the v3 spec tp Spring Cloud contracts. * Implemented conversion of Pact v3 messaging to/from Spring Cloud contracts * Updated code based on comments in the PR * Added null matcher * Added missing null matcher conversion from SCC to Pact * Added number, integer and decimal matchers * Added support for multiple matchers for the same json path. Currently only the AND rule logic is supported. * Added value generator functionality * Refactored `stubMatchers` and `testMatchers` to support multiple types of "matcher groups", eg. header, path & query * Refactored e37a8c5d82f96da6e1a698331afc62e6cf747bd6 in favor of a less invasive change using deprecation * Added header matcher conversion * Add/updated the ASF license header * Fixed an issue with the header matchers while updating the documentation * Updated the documentation * Made some last minute changes to the `bodyMatchers`: `byNull()` isn't supported by WireMock Closes #583 Fixes #595
This commit is contained in:
committed by
Marcin Grzejszczak
parent
ff8fd0a16c
commit
de529e222e
@@ -371,8 +371,11 @@ want to force the consumers to stub their clocks to always return the same value
|
||||
so that it gets matched by the stub.
|
||||
|
||||
For Groovy DSL you can provide the dynamic parts in your contracts
|
||||
in two ways: pass them directly in the body or set them in separate sections called
|
||||
`testMatchers` and `stubMatchers`.
|
||||
in two ways: pass them directly in the body or set them in a separate section called
|
||||
`bodyMatchers`.
|
||||
|
||||
NOTE: Before 2.0.0 these were set using `testMatchers` and `stubMatchers`,
|
||||
check out the https://github.com/spring-cloud/spring-cloud-contract/wiki/Spring-Cloud-Contract-2.0-Migration-Guide[migration guide] for more information.
|
||||
|
||||
For YAML you can only use the `matchers` section.
|
||||
|
||||
@@ -732,11 +735,12 @@ If you work with https://docs.pact.io/[Pact], the following discussion may seem
|
||||
Quite a few users are used to having a separation between the body and setting the
|
||||
dynamic parts of a contract.
|
||||
|
||||
You can use two separate sections:
|
||||
You can use the `bodyMatchers` section for two reasons:
|
||||
|
||||
* `stubMatchers`, which lets you define the dynamic values that should end up in a stub.
|
||||
* Define the dynamic values that should end up in a stub.
|
||||
You can set it in the `request` or `inputMessage` part of your contract.
|
||||
* `testMatchers`, which is present in the `response` or `outputMessage` side of the
|
||||
* Verify the result of your test.
|
||||
This section is present in the `response` or `outputMessage` side of the
|
||||
contract.
|
||||
|
||||
Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the
|
||||
@@ -744,7 +748,7 @@ following matching possibilities:
|
||||
|
||||
.Groovy DSL
|
||||
|
||||
* For `stubMatchers`:
|
||||
* For the stubs:
|
||||
** `byEquality()`: The value taken from the response via the provided JSON Path must be
|
||||
equal to the value provided in the contract.
|
||||
** `byRegex(...)`: The value taken from the response via the provided JSON Path must
|
||||
@@ -755,7 +759,7 @@ match the regex for an ISO Date value.
|
||||
match the regex for an ISO DateTime value.
|
||||
** `byTime()`: The value taken from the response via the provided JSON Path must
|
||||
match the regex for an ISO Time value.
|
||||
* For `testMatchers`:
|
||||
* For the verification:
|
||||
** `byEquality()`: The value taken from the response via the provided JSON Path must be
|
||||
equal to the provided value in the contract.
|
||||
** `byRegex(...)`: The value taken from the response via the provided JSON Path must
|
||||
@@ -781,6 +785,7 @@ following, depending on the JSON path:
|
||||
*** `Map`: If you point to a `Map`.
|
||||
*** `Number`: If you point to `Integer`, `Double`, or other kind of number.
|
||||
*** `Boolean`: If you point to a `Boolean`.
|
||||
** `byNull()`: The value taken from the response via the provided JSON Path must be null
|
||||
|
||||
.YAML
|
||||
|
||||
@@ -824,6 +829,7 @@ Below you can find the allowed list of `type`s.
|
||||
** `by_type`
|
||||
*** there are 2 additional fields accepted: `minOccurrence` and `maxOccurrence`.
|
||||
** `by_command`
|
||||
** `by_null`
|
||||
|
||||
Consider the following example:
|
||||
|
||||
@@ -846,7 +852,7 @@ contain are explicitly set. For the `valueWithoutAMatcher`, the verification tak
|
||||
in the same way as without the use of matchers. In that case, the test performs an
|
||||
equality check.
|
||||
|
||||
For the response side in the `testMatchers` section, we define the dynamic parts in a
|
||||
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
|
||||
@@ -868,7 +874,7 @@ separates the autogenerated assertions and the assertion from matchers):
|
||||
// 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\"}");
|
||||
.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)
|
||||
@@ -879,29 +885,30 @@ separates the autogenerated assertions and the assertion from matchers):
|
||||
assertThat(response.header("Content-Type")).matches("application/json.*");
|
||||
// and:
|
||||
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
|
||||
assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo");
|
||||
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+)?");
|
||||
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)).hasSizeGreaterThanOrEqualTo(1);
|
||||
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)).hasSizeLessThanOrEqualTo(3);
|
||||
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)).hasSizeBetween(1, 3);
|
||||
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)).hasSizeGreaterThanOrEqualTo(0);
|
||||
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)).hasSizeLessThanOrEqualTo(0);
|
||||
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");
|
||||
----
|
||||
|
||||
IMPORTANT: Notice that, for the `byCommand` method, the example calls the
|
||||
@@ -917,7 +924,7 @@ The resulting WireMock stub is in the following example:
|
||||
include::{plugins_path}/spring-cloud-contract-converters/src/test/groovy/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockClientConverterSpec.groovy[tags=matchers,indent=0]
|
||||
----
|
||||
|
||||
IMPORTANT: If you use a `matcher`, then the part of the request aned response that the
|
||||
IMPORTANT: If you use a `matcher`, then 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.
|
||||
@@ -944,7 +951,7 @@ Contract.make {
|
||||
]
|
||||
]
|
||||
)
|
||||
testMatchers {
|
||||
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('.+'))
|
||||
@@ -999,7 +1006,7 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
|
||||
|
||||
If you're using asynchronous communication on the server side (your controllers are
|
||||
returning `Callable`, `DeferredResult`, and so on), then, inside your contract, you must
|
||||
provide a `sync()` method in the `response` section. The following code shows an example:
|
||||
provide an `async()` method in the `response` section. The following code shows an example:
|
||||
|
||||
.Groovy DSL
|
||||
[source,groovy,indent=0]
|
||||
@@ -1378,8 +1385,15 @@ org.springframework.cloud.contract.verifier.converter.YamlContractConverter
|
||||
==== Pact Converter
|
||||
|
||||
Spring Cloud Contract includes support for https://docs.pact.io/[Pact] representation of
|
||||
contracts. Instead of using the Groovy DSL, you can use Pact files. In this section, we
|
||||
present how to add Pact support for your project.
|
||||
contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we
|
||||
present how to add Pact support for your project. Note however that not all functionality is supported.
|
||||
Starting with v3 you can combine multiple matcher for the same element;
|
||||
you can use matchers for the body, headers, request and path; and you can use value generators.
|
||||
Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic.
|
||||
Next to that the request and path matchers are skipped during the conversion.
|
||||
When using a date, time or datetime value generator with a given format,
|
||||
the given format will be skipped and the ISO format will be used.
|
||||
|
||||
|
||||
==== Pact Contract
|
||||
|
||||
@@ -1395,7 +1409,7 @@ The remainder of this section about using Pact refers to the preceding file.
|
||||
|
||||
==== Pact for Producers
|
||||
|
||||
On the producer side, you mustadd two additional dependencies to your plugin
|
||||
On the producer side, you must add two additional dependencies to your plugin
|
||||
configuration. One is the Spring Cloud Contract Pact support, and the other represents
|
||||
the current Pact version that you use.
|
||||
|
||||
@@ -1429,10 +1443,10 @@ test might be as follows:
|
||||
|
||||
// then:
|
||||
assertThat(response.statusCode()).isEqualTo(200);
|
||||
assertThat(response.header("Content-Type")).isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8");
|
||||
assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
|
||||
// and:
|
||||
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
|
||||
assertThatJson(parsedJson).field("rejectionReason").isEqualTo("Amount too high");
|
||||
assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
|
||||
// and:
|
||||
assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
|
||||
}
|
||||
@@ -1443,17 +1457,18 @@ The corresponding generated stub might be as follows:
|
||||
[source,javascript,indent=0]
|
||||
----
|
||||
{
|
||||
"id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
|
||||
"uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
|
||||
"request" : {
|
||||
"url" : "/fraudcheck",
|
||||
"method" : "PUT",
|
||||
"headers" : {
|
||||
"Content-Type" : {
|
||||
"equalTo" : "application/vnd.fraud.v1+json"
|
||||
"matches" : "application/vnd\\.fraud\\.v1\\+json.*"
|
||||
}
|
||||
},
|
||||
"bodyPatterns" : [ {
|
||||
"matchesJsonPath" : "$[?(@.loanAmount == 99999)]"
|
||||
"matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]"
|
||||
}, {
|
||||
"matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
|
||||
} ]
|
||||
@@ -1463,8 +1478,9 @@ The corresponding generated stub might be as follows:
|
||||
"body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
|
||||
"headers" : {
|
||||
"Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transformers" : [ "response-template" ]
|
||||
},
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user