All of the aforementioned approaches are equal. That means that stub and client methods are aliases over the consumer
method. Let’s take a closer look at what we can do with those values in the subsequent sections.
diff --git a/spring-cloud-contract.html b/spring-cloud-contract.html index 1ca9320954..09dd25e512 100644 --- a/spring-cloud-contract.html +++ b/spring-cloud-contract.html @@ -755,11 +755,16 @@ $(addBlockSwitches);
ignored property on the contract itself:
The contract can contain some dynamic properties - timestamps / ids etc. You don’t want to enforce the consumers to stub their clocks to always return the same value of time so that it gets matched by the stub. That’s why we allow you to provide the dynamic -parts in your contracts in the following way
+parts in your contracts in two ways. One is to pass them directly in the +body and one to set them in a separate section calledtestMatchers and stubMatchers.
either via the value method
You can set the properties inside the body either via the value method
All of the aforementioned approaches are equal. That means that stub and client methods are aliases over the consumer
method. Let’s take a closer look at what we can do with those values in the subsequent sections.
You can use regular expressions to write your requests in Contract DSL. It is particularly useful when you want to indicate that a given response should be provided for requests that follow a given pattern. Also, you can use it when you need to use patterns and not exact values both @@ -6584,8 +6591,8 @@ String isoTime() {
It is possible to provide optional parameters in your contract. It’s only possible to have optional parameter for the:
It is also possible to define a method call to be executed on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" in the configuration. Please see the examples below:
If you’ve been working with Pact this might seem familiar. Quite a few users +are used to having a separation between the body and setting dynamic parts of your contract.
+That’s why you can profit from two separate sections. One is called stubMatchers where you can
+define the dynamic values that should end up in a stub. You can set it in the request or inputMessage
+part of your contract. The other is called testMatchers which is present in the response or
+outputMessage side of the contract.
Currently we support only JSON Path based matchers with the following matching possibilities.
+For stubMatchers:
byRegex(…) - the value taken from the response via the provided JSON Path needs
+to match the regex
byDate() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO Date
byTimestamp() - the value taken from the response via the provided JSON Path needs
+to match the regex for ISO DateTime
byTime() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO Time
For testMatchers:
byRegex(…) - the value taken from the response via the provided JSON Path needs
+to match the regex
byDate() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO Date
byTimestamp() - the value taken from the response via the provided JSON Path needs
+to match the regex for ISO DateTime
byTime() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO Time
byType() - the value taken from the response via 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 where you can set minOccurrence and maxOccurrence.
+That way you can assert on the size of the collection.
Let’s take a look at the following example:
+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"
+ ])
+ stubMatchers {
+ jsonPath('$.duck', byRegex("[0-9]{3}"))
+ jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
+ jsonPath('$.number', byRegex(number()))
+ jsonPath('$.aBoolean', byRegex(anyBoolean()))
+ jsonPath('$.date', byDate())
+ jsonPath('$.dateTime', byTimestamp())
+ jsonPath('$.time', byTime())
+ }
+ headers {
+ contentType(applicationJson())
+ }
+ }
+ 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
+ ],
+ ])
+ testMatchers {
+ // asserts the jsonpath value against manual regex
+ jsonPath('$.duck', byRegex("[0-9]{3}"))
+ // asserts the jsonpath value against some default regex
+ jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
+ jsonPath('$.number', byRegex(number()))
+ jsonPath('$.aBoolean', byRegex(anyBoolean()))
+ // 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)
+ })
+ }
+ headers {
+ contentType(applicationJson())
+ }
+ }
+}
+In this example we’re providing the dynamic portions of the contract in the matchers sections.
+ For the request part you can see that for all fields but valueWithoutAMatcher we’re setting
+ explicitly the values of regular expressions we’d like the stub to contain. For the valueWithoutAMatcher
+ the verification will take place in the same way as without the usage of matchers - the test
+ will perform an equality check in this case.
For the response side in the testMatchers section we’re defining all the dynamic parts
+ in a similar manner. The only difference is that we have the byType matchers too. In that
+ case we’re checking 4 fields in the way that we’re verifying whether the response from the test
+ has a value whose JSON path matching the given field is of the same type as the one defined in the response body and:
for $.valueWithTypeMatch - we’re just checking the whether the type is the same
for $.valueWithMin - we’re checking the type and assert if the size is greater or equal to the min occurrence
for $.valueWithMax - we’re checking the type and assert if the size is smaller or equal to the max occurrence
for $.valueWithMinMax - we’re checking the type and assert if the size is between the min and max occurrence
The resulting test would look more or less like this (note that we’re separating the autogenerated
+assertions and the one from matchers with an and section):
// 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\"}");
+
+// 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("$.alpha", String.class)).matches("[\\p{L}]*");
+ assertThat(parsedJson.read("$.number", String.class)).matches("-?\\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(class java.lang.String.class);
+ assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
+ assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isLessThanOrEqualTo(1);
+ assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
+ assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isGreaterThanOrEqualTo(3);
+ assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
+ assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3);
+and the WireMock stub like this:
+ '''
+{
+ "request" : {
+ "urlPath" : "/get",
+ "method" : "GET",
+ "headers" : {
+ "Content-Type" : {
+ "matches" : "application/json.*"
+ }
+ },
+ "bodyPatterns" : [ {
+ "matchesJsonPath" : "$[?(@.valueWithoutAMatcher == 'foo')]"
+ }, {
+ "matchesJsonPath" : "$[?(@.valueWithTypeMatch == 'string')]"
+ }, {
+ "matchesJsonPath" : "$.list.some.nested[?(@.anothervalue == 4)]"
+ }, {
+ "matchesJsonPath" : "$.list.someother.nested[?(@.anothervalue == 4)]"
+ }, {
+ "matchesJsonPath" : "$.list.someother.nested[?(@.json == 'with value')]"
+ }, {
+ "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
+ }, {
+ "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
+ }, {
+ "matchesJsonPath" : "$[?(@.number =~ /(-?\\\\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 =~ /(.*)/)]"
+ } ]
+ },
+ "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]}",
+ "headers" : {
+ "Content-Type" : "application/json"
+ }
+ }
+}
+'''
+