@@ -1973,28 +1978,16 @@ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
package com.example.fraud;
-import org.junit.Before;
-
+import com.example.fraud.FraudDetectionController;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
+import org.junit.Before;
+
public class FraudBase {
@Before
public void setup() {
- RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
- new FraudStatsController(stubbedStatsProvider()));
- }
-
- private StatsProvider stubbedStatsProvider() {
- return fraudType -> {
- switch (fraudType) {
- case DRUNKS:
- return 100;
- case ALL:
- return 200;
- }
- return 0;
- };
+ RestAssuredMockMvc.standaloneSetup(new FraudDetectionController());
}
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
@@ -2495,7 +2488,7 @@ consumer will you break with your local changes.
Let’s assume that we have a producer with coordinates com.example:server and 3 consumers: client1,
client2, client3. Then in the repository with common contracts you would have the following setup
-(which you can checkout here:
+(which you can checkout
here:
@@ -2550,8 +2543,8 @@ one to one to the contents of the repo.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
- <spring-cloud-contract.version>1.1.0.BUILD-SNAPSHOT</spring-cloud-contract.version>
- <spring-cloud-dependencies.version>Dalston.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
+ <spring-cloud-contract.version>1.0.4.BUILD-SNAPSHOT</spring-cloud-contract.version>
+ <spring-cloud-dependencies.version>Camden.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
</properties>
<dependencyManagement>
@@ -4321,6 +4314,25 @@ For messaging, special stub routes are defined.
Running stubs
+
Limitations
+
+
+
+|
+ Important
+ |
+
+There might be a problem with StubRunner shutting down ports between tests. You might
+ have a situation in which you get port conflicts. As long as you use the same context across tests
+ everything works fine. But when the context are different (e.g. different stubs or different profiles)
+ then you have to either use @DirtiesContext to shut down the stub servers, or else run them on
+ different ports per test.
+ |
+
+
+
+
+
Running using main app
You can set the following options to the main class:
@@ -6087,7 +6099,7 @@ a tiny subset of it (namely literals, method calls and closures). What’s m
-
Limitations
+
Limitations
@@ -6371,10 +6383,13 @@ or just set the 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 called
testMatchers and
stubMatchers.
+
+
Dynamic properties inside the body
-
either via the value method
+
You can set the properties inside the body either via the value method
@@ -6399,9 +6414,8 @@ $(client(...), server(...))
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.
-
-
-
Regular expressions
+
+
Regular expressions
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
@@ -6478,11 +6492,14 @@ provide the generated string that matches the provided regular expression. For e
protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/)
protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/)
protected static final Pattern NUMBER = Pattern.compile('-?\\d*(\\.\\d+)?')
-protected static final Pattern IP_ADDRESS = Pattern.compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])');
-protected static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?');
+protected static final Pattern IP_ADDRESS = Pattern.compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])')
+protected static final Pattern HOSTNAME_PATTERN = Pattern.compile('((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?')
protected static final Pattern EMAIL = Pattern.compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}');
protected static final Pattern URL = Pattern.compile('((www\\.|(http|https|ftp|news|file)+\\:\\/\\/)[_.a-z0-9-]+\\.[a-z0-9\\/_:@=.+?,##%&~-]*[^.|\\\'|\\# |!|\\(|?|,| |>|<|;|\\)])')
protected static final Pattern UUID = Pattern.compile('[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}')
+protected static final Pattern ANY_DATE = Pattern.compile('(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])')
+protected static final Pattern ANY_DATE_TIME = Pattern.compile('([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])')
+protected static final Pattern ANY_TIME = Pattern.compile('(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
String onlyAlphaUnicode() {
return ONLY_ALPHA_UNICODE.pattern()
@@ -6514,6 +6531,18 @@ String url() {
String uuid(){
return UUID.pattern()
+}
+
+String isoDate() {
+ return ANY_DATE.pattern()
+}
+
+String isoDateTime() {
+ return ANY_DATE_TIME.pattern()
+}
+
+String isoTime() {
+ return ANY_TIME.pattern()
}
@@ -6549,8 +6578,8 @@ String uuid(){
-
-
Passing optional parameters
+
+
Passing optional parameters
It is possible to provide optional parameters in your contract. It’s only possible to have optional parameter for the:
@@ -6655,14 +6684,15 @@ String uuid(){
-
-
Executing custom methods on server side
+
+
Executing custom methods on server side
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:
+in the configuration. Example:
+
+
-
-
Contract DSL
org.springframework.cloud.contract.spec.Contract.make {
@@ -6688,9 +6718,9 @@ in the configuration. Please see the examples below:
}
+
-
-
Base Mock Spec
abstract class BaseMockMvcSpec extends Specification {
@@ -6712,6 +6742,302 @@ in the configuration. Please see the examples below:
+
+
Dynamic properties in matchers sections
+
+
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:
+
+
+
+-
+
byEquality() - the value taken from the response via the provided JSON Path needs
+to be equal to the provided value in the contract
+
+-
+
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
+
+
+
+
+
+
+-
+
byEquality() - the value taken from the response via the provided JSON Path needs
+to be equal to the provided value in the contract
+
+-
+
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('$.duck', byEquality())
+ jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
+ jsonPath('$.alpha', byEquality())
+ 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 the provided value
+ jsonPath('$.duck', byEquality())
+ // asserts the jsonpath value against some default regex
+ jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
+ jsonPath('$.alpha', byEquality())
+ 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("$.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("$.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(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isGreaterThanOrEqualTo(1);
+ assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
+ assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isLessThanOrEqualTo(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" : "$[?(@.duck == 123)]"
+ }, {
+ "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
+ }, {
+ "matchesJsonPath" : "$[?(@.alpha == 'abc')]"
+ }, {
+ "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"
+ }
+ }
+}
+'''
+
+
+
+
JAX-RS support
@@ -6783,6 +7109,107 @@ section a async() method. Example:
+
Working with Context Paths
+
+
Spring Cloud Contract supports context paths.
+
+
+
+
+|
+ Important
+ |
+
+The only thing that changes in order to fully support context paths is the switch
+on the PRODUCER side. The autogenerated tests need to be using the EXPLICIT mode.
+ |
+
+
+
+
+
The consumer side remains untouched, in order for the generated test to pass you have to switch the EXPLICIT mode.
+
+
+
Maven
+
+
<plugin>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-contract-maven-plugin</artifactId>
+ <version>${spring-cloud-contract.version}</version>
+ <extensions>true</extensions>
+ <configuration>
+ <testMode>EXPLICIT</testMode>
+ </configuration>
+</plugin>
+
+
+
+
Gradle
+
+
contracts {
+ testMode = 'EXPLICIT'
+}
+
+
+
+
That way you’ll generate a test that DOES NOT use MockMvc. It means that you’re generating
+real requests and you need to setup your generated test’s base class to work on a real socket.
+
+
+
Let’s imagine the following contract:
+
+
+
+
org.springframework.cloud.contract.spec.Contract.make {
+ request {
+ method 'GET'
+ url '/my-context-path/url'
+ }
+ response {
+ status 200
+ }
+}
+
+
+
+
Here is an example of how to set up a base class and Rest Assured for everything to work correctly.
+
+
+
+
import com.jayway.restassured.RestAssured;
+import org.junit.Before;
+import org.springframework.boot.context.embedded.LocalServerPort;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class ContextPathTestingBaseClass {
+
+ @LocalServerPort int port;
+
+ @Before
+ public void setup() {
+ RestAssured.baseURI = "http://localhost";
+ RestAssured.port = this.port;
+ }
+}
+
+
+
+
+
+-
+
all your requests in the autogenerated tests will be sent to the real endpoint with your context path included (e.g. /my-context-path/url)
+
+-
+
your contracts reflect that you have a context path, thus your generated stubs will also
+have that information (e.g. in the stubs you’ll see that you have too call /my-context-path/url)
+
+
+
+
+
Messaging Top-Level Elements
The DSL for messaging looks a little bit different than the one that focuses on HTTP.