diff --git a/2.0.x/images/callouts/1.png b/2.0.x/images/callouts/1.png new file mode 100644 index 0000000000..7d473430b7 Binary files /dev/null and b/2.0.x/images/callouts/1.png differ diff --git a/2.0.x/images/callouts/2.png b/2.0.x/images/callouts/2.png new file mode 100644 index 0000000000..5d09341b2f Binary files /dev/null and b/2.0.x/images/callouts/2.png differ diff --git a/2.0.x/images/callouts/3.png b/2.0.x/images/callouts/3.png new file mode 100644 index 0000000000..ef7b700471 Binary files /dev/null and b/2.0.x/images/callouts/3.png differ diff --git a/2.0.x/multi/images/callouts/1.png b/2.0.x/multi/images/callouts/1.png new file mode 100644 index 0000000000..7d473430b7 Binary files /dev/null and b/2.0.x/multi/images/callouts/1.png differ diff --git a/2.0.x/multi/images/callouts/2.png b/2.0.x/multi/images/callouts/2.png new file mode 100644 index 0000000000..5d09341b2f Binary files /dev/null and b/2.0.x/multi/images/callouts/2.png differ diff --git a/2.0.x/multi/images/callouts/3.png b/2.0.x/multi/images/callouts/3.png new file mode 100644 index 0000000000..ef7b700471 Binary files /dev/null and b/2.0.x/multi/images/callouts/3.png differ diff --git a/2.0.x/multi/multi__contract_dsl.html b/2.0.x/multi/multi__contract_dsl.html index f500fc466e..8303a19275 100644 --- a/2.0.x/multi/multi__contract_dsl.html +++ b/2.0.x/multi/multi__contract_dsl.html @@ -1,15 +1,14 @@
-![]() | Important | |||||
|---|---|---|---|---|---|---|
Remember that, inside the contract file, you have to provide the fully + Spring Cloud Contract supports out of the box 2 types of DSL. One written in
+ If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy +before. Knowledge of the language is not really needed, as the Contract DSL uses only a +tiny subset of it (only literals, method calls and closures). Also, the DSL is statically +typed, to make it programmer-readable without any knowledge of the DSL itself.
Contract DSL is written in Groovy, but do not be alarmed if you have not used Groovy -before. Knowledge of the language is not really needed, as the Contract DSL uses only a -tiny subset of it (only literals, method calls and closures). Also, the DSL is statically -typed, to make it programmer-readable without any knowledge of the DSL itself.
The Contract is present in the The following is a complete example of a contract definition: org.springframework.cloud.contract.spec.Contract.make {
+ |
![]() | Tip |
|---|---|
Spring Cloud Contract supports defining multiple contracts in a single file. |
The following is a complete example of a Groovy contract definition:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/api/12'
@@ -48,18 +47,66 @@ Cloud Contract Verifier repository.The following is a complete exampl
'''
}
response {
- status 200
+ status OK()
}
-}
![]() | Note |
|---|---|
The preceding example does not contain all the features of the DSL appear. The -remainder of this section describes the other features. |
You can compile Contracts to WireMock stubs mapping using standalone maven command:
-mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert
![]() | Warning | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Spring Cloud Contract Verifier does not properly support XML. Please use JSON or +} The following is a complete example of a YAML contract definition: description: Some description +name: some name +priority: 8 +ignored: true +request: + url: /foo + queryParameters: + a: b + b: c + method: PUT + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + nullValue: null + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + - path: $.nullValue + type: by_null + value: null + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it)
The following sections describe the most common top-level elements: You can add a org.springframework.cloud.contract.spec.Contract.make {
+following code shows an example:You can provide a name for your contract. Assume that you provided the following name: + } + YAML. + description: Some description +name: some name +priority: 8 +ignored: true +request: + url: /foo + queryParameters: + a: b + b: c + method: PUT + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + nullValue: null + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + - path: $.nullValue + type: by_null + value: null + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) + You can provide a name for your contract. Assume that you provided the following name:
|
Groovy DSL. +
org.springframework.cloud.contract.spec.Contract.make {
+ name("some_special_name")
+}+
YAML. +
name: some name
+
If you want to ignore a contract, you can either set a value of ignored contracts in the
+plugin configuration or set the ignored property on the contract itself:
Groovy DSL. +
org.springframework.cloud.contract.spec.Contract.make {
ignored()
-}Starting with version 1.2.0, you can pass values from files. Assume that you have the
+}
+
YAML. +
ignored: true
+
Starting with version 1.2.0, you can pass values from files. Assume that you have the
following resources in our project.
└── src
└── test
└── resources
└── contracts
├── readFromFile.groovy
├── request.json
- └── response.jsonFurther assume that your contract is as follows:
import org.springframework.cloud.contract.spec.Contract
+ └── response.jsonFurther assume that your contract is as follows:
Groovy DSL. +
import org.springframework.cloud.contract.spec.Contract Contract.make { request { @@ -96,17 +205,26 @@ Contract.make { url("/1") } response { - status 200 + status OK() body(file("response.json")) headers { contentType(textPlain()) } } -}
Further assume that the JSON files is as follows:
request.json
{ "status" : "REQUEST" }
response.json
{ "status" : "RESPONSE" }
When test or stub generation takes place, the contents of the file is passed to the body
-of a request or a response. That works because of the file(…) method. The argument of
-that method needs to be a file with location relative to the folder in which the contract
-lays.
The following methods can be called in the top-level closure of a contract definition.
-request and response are mandatory. priority is optional.
org.springframework.cloud.contract.spec.Contract.make {
+}+
YAML. +
request: + method: GET + url: /foo + bodyFromFile: request.json +response: + status: 200 + bodyFromFile: response.json
+
Further assume that the JSON files is as follows:
request.json
{ "status" : "REQUEST" }
response.json
{ "status" : "RESPONSE" }
When test or stub generation takes place, the contents of the file is passed to the body +of a request or a response. The name of the file needs to be a file with location +relative to the folder in which the contract lays.
The following methods can be called in the top-level closure of a contract definition.
+request and response are mandatory. priority is optional.
Groovy DSL. +
org.springframework.cloud.contract.spec.Contract.make {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
@@ -125,8 +243,18 @@ lays.The HTTP protocol requires only method and address to be specified in a request. The -same information is mandatory in request definition of the Contract.
org.springframework.cloud.contract.spec.Contract.make {
+}+
YAML. +
priority: 8 +request: +... +response: +...
+
![]() | Important |
|---|---|
If you want to make your contract have a higher value of priority
+you need to pass a lower number to the |
The HTTP protocol requires only method and url to be specified in a request. The +same information is mandatory in request definition of the Contract.
Groovy DSL. +
org.springframework.cloud.contract.spec.Contract.make {
request {
// HTTP request method (GET/POST/PUT/DELETE).
method 'GET'
@@ -138,8 +266,13 @@ same information is mandatory in request definition of the Contract.//...
}
-}
It is possible to specify an absolute rather than relative url, but using urlPath is
-the recommended way, as doing so makes the tests host-independent.
org.springframework.cloud.contract.spec.Contract.make {
+}
+
YAML.
+
method: PUT
+url: /foo
+
It is possible to specify an absolute rather than relative url, but using urlPath is
+the recommended way, as doing so makes the tests host-independent.
Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
@@ -150,8 +283,13 @@ the recommended way, as doing so makes the tests ho
response {
//...
}
-}request may contain query parameters, which are specified in a closure nested in a
-call to urlPath or url.
org.springframework.cloud.contract.spec.Contract.make {
+}
+
YAML.
+
request:
+ method: PUT
+ urlPath: /foo
+
request may contain query parameters.
Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
@@ -191,7 +329,62 @@ call to urlPath or url
response {
//...
}
-}request may contain additional request headers, as shown in the following example:
org.springframework.cloud.contract.spec.Contract.make {
+}
+
YAML.
+
request:
+...
+ queryParameters:
+ a: b
+ b: c
+ headers:
+ foo: bar
+ fooReq: baz
+ cookies:
+ foo: bar
+ fooReq: baz
+ body:
+ foo: bar
+ matchers:
+ body:
+ - path: $.foo
+ type: by_regex
+ value: bar
+ headers:
+ - key: foo
+ regex: bar
+response:
+ status: 200
+ headers:
+ foo2: bar
+ foo3: foo33
+ fooRes: baz
+ body:
+ foo2: bar
+ foo3: baz
+ nullValue: null
+ matchers:
+ body:
+ - path: $.foo2
+ type: by_regex
+ value: bar
+ - path: $.foo3
+ type: by_command
+ value: executeMe($it)
+ - path: $.nullValue
+ type: by_null
+ value: null
+ headers:
+ - key: foo2
+ regex: bar
+ - key: foo3
+ command: andMeToo($it)
+ cookies:
+ - key: foo2
+ regex: bar
+ - key: foo3
+ predefined:
+
request may contain additional request headers, as shown in the following example:
Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
@@ -208,7 +401,40 @@ call to urlPath or url
response {
//...
}
-}request may contain a request body, as shown in the following example:
org.springframework.cloud.contract.spec.Contract.make {
+}
+
YAML.
+
request:
+...
+headers:
+ foo: bar
+ fooReq: baz
+
request may contain additional request cookies, as shown in the following example:
Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
+ request {
+ //...
+
+ // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
+ // there are also some helper methods
+ cookies {
+ cookie 'key': 'value'
+ cookie('another_key', 'another_value')
+ }
+
+ //...
+ }
+
+ response {
+ //...
+ }
+}
+
YAML.
+
request:
+...
+cookies:
+ foo: bar
+ fooReq: baz
+
request may contain a request body:
Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
@@ -220,8 +446,15 @@ call to urlPath or url
response {
//...
}
-}request may contain multipart elements. To include multipart elements, call the
-multipart() method, as shown in the following example
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
+}
+
YAML.
+
request:
+...
+body:
+ foo: bar
+
request may contain multipart elements. To include multipart elements, use the
+multipart method/section, as shown in the following examples
Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
request {
method "PUT"
url "/multipart"
@@ -238,17 +471,75 @@ call to urlPath or url
// name of the file
name: $(c(regex(nonEmpty())), p('filename.csv')),
// content of the file
- content: $(c(regex(nonEmpty())), p('file content')))
+ content: $(c(regex(nonEmpty())), p('file content')),
+ // content type for the part
+ contentType: $(c(regex(nonEmpty())), p('application/json')))
+ )
+ }
+ response {
+ status OK()
+ }
+}
+org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
+ request {
+ method "PUT"
+ url "/multipart"
+ headers {
+ contentType('multipart/form-data;boundary=AaB03x')
+ }
+ multipart(
+ file: named(
+ name: value(stub(regex('.+')), test('file')),
+ content: value(stub(regex('.+')), test([100, 117, 100, 97] as byte[]))
+ )
)
}
response {
status 200
}
-}In the preceding example, we define parameters in either of two ways:
- Directly, by using the map notation, where the value can be a dynamic property (such as
+}
+
YAML.
+
request:
+ method: PUT
+ url: /multipart
+ headers:
+ Content-Type: multipart/form-data;boundary=AaB03x
+ multipart:
+ params:
+ # key (parameter name), value (parameter value) pair
+ formParameter: '"formParameterValue"'
+ someBooleanParameter: true
+ named:
+ - paramName: file
+ fileName: filename.csv
+ fileContent: file content
+ matchers:
+ multipart:
+ params:
+ - key: formParameter
+ regex: ".+"
+ - key: someBooleanParameter
+ predefined: any_boolean
+ named:
+ - paramName: file
+ fileName:
+ predefined: non_empty
+ fileContent:
+ predefined: non_empty
+response:
+ status: 200
+
In the preceding example, we define parameters in either of two ways:
Groovy DSL
- Directly, by using the map notation, where the value can be a dynamic property (such as
formParameter: $(consumer(…), producer(…))). - By using the
named(…) method that lets you set a named parameter. A named parameter
can set a name and content. You can call it either via a method with two arguments,
such as named("fileName", "fileContent"), or via a map notation, such as
-named(name: "fileName", content: "fileContent").
From this contract, the generated test is as follows:
// given:
+named(name: "fileName", content: "fileContent").
YAML
- The multipart parameters are set via
multipart.params section - The named parameters (the
fileName and fileContent for a given parameter name)
+can be set via the multipart.named section. That section contains
+the paramName (name of the parameter), fileName (name of the file),
+fileContent (content of the file) fields The dynamic bits can be set via the matchers.multipart section
- for parameters use the
params section that can accept
+regex or a predefined regular expression - for named params use the
named section where first you
+define the parameter name via paramName and then you can pass the
+parametrization of either fileName or fileContent via
+regex or a predefined regular expression
From this contract, the generated test is as follows:
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "multipart/form-data;boundary=AaB03x")
.param("formParameter", "\"formParameterValue\"")
@@ -271,11 +562,11 @@ such as named("fileName", "fileContent"), or via a
}
},
"bodyPatterns" : [ {
- "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*"
+ "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*"
}, {
- "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*"
+ "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*"
}, {
- "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n[\\\\S\\\\s]+\\r\\n--\\\\1.*"
+ "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n[\\\\S\\\\s]+\\r\\n--\\\\1.*"
} ]
},
"response" : {
@@ -284,21 +575,31 @@ such as named("fileName", "fileContent"), or via a
}
}
'''
The response must contain an HTTP status code and may contain other information. The -following code shows an example:
org.springframework.cloud.contract.spec.Contract.make {
+following code shows an example:Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
}
response {
// Status code sent by the server
// in response to request specified above.
- status 200
+ status OK()
}
-}Besides status, the response may contain headers and a body, both of which are
-specified the same way as in the request (see the previous paragraph).
The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not +}
+
YAML. +
response: +... +status: 200
+
Besides status, the response may contain headers, cookies and a body, both of which are +specified the same way as in the request (see the previous paragraph).
![]() | Tip |
|---|---|
Via the Groovy DSL you can reference the |
The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not
want to force the consumers to stub their clocks to always return the same value of time
-so that it gets matched by the stub. 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.
You can set the properties inside the body either with the value method or, if you use
+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 a separate section called
+bodyMatchers.
![]() | Note |
|---|---|
Before 2.0.0 these were set using |
For YAML you can only use the matchers section.
![]() | Important |
|---|---|
This section is valid only for Groovy DSL. Check out the +Section 8.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature. |
You can set the properties inside the body either with the value method or, if you use
the Groovy map notation, with $(). The following example shows how to set dynamic
properties with the value method:
value(consumer(...), producer(...)) value(c(...), p(...)) @@ -307,7 +608,8 @@ value(client(...), server(...))
The following example shows how to set d $(c(...), p(...)) $(stub(...), test(...)) $(client(...), server(...))
Both approaches work equally well. stub and client methods are aliases over the consumer
-method. Subsequent sections take a closer look at what you can do with those values.
You can use regular expressions to write your requests in Contract DSL. Doing so is +method. Subsequent sections take a closer look at what you can do with those values.
![]() | Important |
|---|---|
This section is valid only for Groovy DSL. Check out the +Section 8.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature. |
You can use regular expressions to write your requests in Contract DSL. Doing so 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 regular expressions when you need to use patterns and not exact values both for your test and your server side tests.
The following example shows how to use regular expressions to write a request:
org.springframework.cloud.contract.spec.Contract.make {
@@ -316,7 +618,7 @@ need to use patterns and not exact values both for your test and your server sid
url $(consumer(~/\/[0-9]{2}/), producer('/12'))
}
response {
- status 200
+ status OK()
body(
id: $(anyNumber()),
surname: $(
@@ -347,7 +649,7 @@ the provided regular expression. The following code shows an example:200
+ status OK()
body([
responseElement: $(producer(regex('[0-9]{7}')))
])
@@ -358,12 +660,18 @@ the provided regular expression. The following code shows an example:In the preceding example, the opposite side of the communication has the respective data
generated for request and response.
Spring Cloud Contract comes with a series of predefined regular expressions that you can
use in your contracts, as shown in the following example:
protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/)
+protected static final Pattern ALPHA_NUMERIC = Pattern.compile('[a-zA-Z0-9]+')
protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/)
-protected static final Pattern NUMBER = Pattern.compile('-?\\d*(\\.\\d+)?')
+protected static final Pattern NUMBER = Pattern.compile('-?(\\d*\\.\\d+|\\d+)')
+protected static final Pattern INTEGER = Pattern.compile('-?(\\d+)')
+protected static final Pattern POSITIVE_INT = Pattern.compile('([1-9]\\d*)')
+protected static final Pattern DOUBLE = Pattern.compile('-?(\\d*\\.\\d+)')
+protected static final Pattern HEX = Pattern.compile('[a-fA-F0-9]+')
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,6}')
protected static final Pattern URL = UrlHelper.URL
+protected static final Pattern HTTPS_URL = UrlHelper.HTTPS_URL
protected static final Pattern UUID = Pattern.compile('[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-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])')
@@ -380,14 +688,30 @@ Pattern onlyAlphaUnicode() {
return ONLY_ALPHA_UNICODE
}
+Pattern alphaNumeric() {
+ return ALPHA_NUMERIC
+}
+
Pattern number() {
return NUMBER
}
+Pattern positiveInt() {
+ return POSITIVE_INT
+}
+
Pattern anyBoolean() {
return TRUE_OR_FALSE
}
+Pattern anInteger() {
+ return INTEGER
+}
+
+Pattern aDouble() {
+ return DOUBLE
+}
+
Pattern ipAddress() {
return IP_ADDRESS
}
@@ -404,6 +728,10 @@ Pattern url() {
return URL
}
+Pattern httpsUrl() {
+ return HTTPS_URL
+}
+
Pattern uuid(){
return UUID
}
@@ -453,7 +781,8 @@ Pattern nonBlank() {
message: "User not found by email = [${value(producer(regex(email())), consumer('not.existing@user.com'))}]"
)
}
-}
It is possible to provide optional parameters in your contract. However, you can provide +}
![]() | Important |
|---|---|
This section is valid only for Groovy DSL. Check out the +Section 8.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature. |
It is possible to provide optional parameters in your contract. However, you can provide optional parameters only for the following:
The following example shows how to provide optional parameters:
org.springframework.cloud.contract.spec.Contract.make {
priority 1
request {
@@ -496,29 +825,30 @@ expression that must be present 0 or more times.If you use Spock for, the
"""
The following stub would also be generated:
''' { "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" - } - } + "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 == [not.existing@user.com]\\"}", - "headers" : { - "Content-Type" : "application/json" - } + "status" : 404, + "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}", + "headers" : { + "Content-Type" : "application/json" + } }, "priority" : 1 } -'''
You can define a method call that executes on the server side during the test. Such a +'''
![]() | Important |
|---|---|
This section is valid only for Groovy DSL. Check out the +Section 8.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature. |
You can define a method call that executes on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" in the configuration. The following code shows an example of the contract portion of the test case:
org.springframework.cloud.contract.spec.Contract.make {
request {
@@ -538,7 +868,7 @@ following code shows an example of the contract portion of the test case:'/api/12'), producer(regex('^/api/[0-9]{2}$'))),
correlationId: $(consumer('1223456'), producer(execute('isProperCorrelationId($it)')))
)
- status 200
+ status OK()
}
} The following code shows the base class portion of the test case:
abstract class BaseMockMvcSpec extends Specification { @@ -569,7 +899,7 @@ is applied for the whole body - not for parts of it.
The preceding example results in calling the hashCode() method in the request body.
It should resemble the following code:
// given: @@ -582,11 +912,17 @@ It should resemble the following code:// then: assertThat(response.statusCode()).isEqualTo(200);
The best situation is to provide fixed values, but sometimes you need to reference a
-request in your response. To do so, you can use the fromRequest() method, which lets
+request in your response.
If you’re writing contracts using Groovy DSL, you can use the fromRequest() method, which lets
you reference a bunch of elements from the HTTP request. You can use the following
options:
fromRequest().url(): Returns the request URL and query parameters.fromRequest().query(String key): Returns the first query parameter with a given name.fromRequest().query(String key, int index): Returns the nth query parameter with a
given name.fromRequest().path(): Returns the full path.fromRequest().path(int index): Returns the nth path element.fromRequest().header(String key): Returns the first header with a given name.fromRequest().header(String key, int index): Returns the nth header with a given name.fromRequest().body(): Returns the full request body.fromRequest().body(String jsonPath): Returns the element from the request that
-matches the JSON Path.Consider the following contract:
Contract contractDsl = Contract.make {
+matches the JSON Path.If you’re using the YAML contract definition you have to use the
+Handlebars {{{ }}} notation with custom, Spring Cloud Contract
+ functions to achieve this.
{{{ request.url }}}: Returns the request URL and query parameters.{{{ request.query.key.[index] }}}: Returns the nth query parameter with a given name.
+E.g. for key foo, first entry {{{ request.query.foo.[0] }}}{{{ request.path }}}: Returns the full path.{{{ request.path.[index] }}}: Returns the nth path element. E.g.
+for first entry `{{{ request.path.[0] }}}{{{ request.headers.key }}}: Returns the first header with a given name.{{{ request.headers.key.[index] }}}: Returns the nth header with a given name.{{{ request.body }}}: Returns the full request body.{{{ jsonpath this 'your.json.path' }}}: Returns the element from the request that
+matches the JSON Path. E.g. for json path $.foo - {{{ jsonpath this '$.foo' }}}Consider the following contract:
Groovy DSL. +
Contract contractDsl = Contract.make {
request {
method 'GET'
url('/api/v1/xxxx') {
@@ -602,7 +938,7 @@ matches the JSON Path.Consider the following contract:
Consider the following contract:
Running a JUnit test generation leads to a test that resembles the following example:
// given:
+}+
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"+
Running a JUnit test generation leads to a test that resembles the following example:
// given: MockMvcRequestSpecification request = given() .header("Authorization", "secret") .header("Authorization", "secret2") @@ -697,7 +1065,9 @@ provide your own extensions, you can register an implementation of theorg.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensionsinterface. Since we use the spring.factories extension approach, you can create an entry inMETA-INF/spring.factoriesfile similar to the following:org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions=\ -org.springframework.cloud.contract.stubrunner.provider.wiremock.TestWireMockExtensionsThe following is an example of a custom extension:
TestWireMockExtensions.groovy. +org.springframework.cloud.contract.stubrunner.provider.wiremock.TestWireMockExtensions +org.springframework.cloud.contract.spec.ContractConverter=\ +org.springframework.cloud.contract.stubrunner.TestCustomYamlContractConverter
The following is an example of a custom extension:
TestWireMockExtensions.groovy.
package org.springframework.cloud.contract.verifier.dsl.wiremock import com.github.tomakehurst.wiremock.extension.Extension @@ -723,30 +1093,39 @@ org.springframework.cloud.contract.stubrunner.provider.wiremock.TestWireMockExte } }
![]() | Important |
|---|---|
Remember to override the |
If you work with Pact, the following discussion may seem familiar. +want the transformation to be applied only for a mapping that explicitly requires it.
If you work with Pact, the following discussion may seem familiar. 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:
stubMatchers, which lets you 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
+dynamic parts of a contract.You can use the bodyMatchers section for two reasons:
request or inputMessage part of your contract.response or outputMessage side of the
contract.Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the -following matching possibilities:
For stubMatchers:
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
-match the regex.byDate(): The value taken from the response via the provided JSON Path must
-match the regex for an ISO Date value.byTimestamp(): The value taken from the response via the provided JSON Path must
-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:
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
-match the regex.byDate(): The value taken from the response via the provided JSON Path must match
-the regex for an ISO Date value.byTimestamp(): The value taken from the response via the provided JSON Path must
-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.byType(): The value taken from the response via the provided JSON Path needs to be
+following matching possibilities:Groovy DSL
For the stubs(in tests on the Consumer’s side):
byEquality(): The value taken from the consumer’s request via the provided JSON Path must be
+equal to the value provided in the contract.byRegex(…): The value taken from the consumer’s request via the provided JSON Path must
+match the regex.byDate(): The value taken from the consumer’s request via the provided JSON Path must
+match the regex for an ISO Date value.byTimestamp(): The value taken from the consumer’s request via the provided JSON Path must
+match the regex for an ISO DateTime value.byTime(): The value taken from the consumer’s request via the provided JSON Path must
+match the regex for an ISO Time value.For the verification(in generated tests on the Producer’s side):
byEquality(): The value taken from the producer’s response via the provided JSON Path must be
+equal to the provided value in the contract.byRegex(…): The value taken from the producer’s response via the provided JSON Path must
+match the regex.byDate(): The value taken from the producer’s response via the provided JSON Path must match
+the regex for an ISO Date value.byTimestamp(): The value taken from the producer’s response via the provided JSON Path must
+match the regex for an ISO DateTime value.byTime(): The value taken from the producer’s response via the provided JSON Path must match
+the regex for an ISO Time value.byType(): The value taken from the producer’s 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, in which you can set minOccurrence and maxOccurrence.
That way, you can assert the size of the flattened collection. To check the size of an
-unflattened collection, use a custom method with the byCommand(…) testMatcher.byCommand(…): The value taken from the response via the provided JSON Path is
+unflattened collection, use a custom method with the byCommand(…) testMatcher.
byCommand(…): The value taken from the producer’s response via the provided JSON Path is
passed as an input to the custom method that you provide. For example,
byCommand('foo($it)') results in calling a foo method to which the value matching the
JSON Path gets passed. The type of the object read from the JSON can be one of the
-following, depending on the JSON path:
String: If you point to a String value.JSONArray: If you point to a List.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.Consider the following example:
Contract contractDsl = Contract.make {
+following, depending on the JSON path:String: If you point to a String value.JSONArray: If you point to a List.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 nullYAML. Please read the Groovy section for detailed explanation of +what the types mean
For YAML the structure of a matcher looks like this
- path: $.foo + type: by_regex + value: bar
Or if you want to use one of the predefined regular expressions
+[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
Below you can find the allowed list of `type`s.
For stubMatchers:
by_equalityby_regexby_dateby_timestampby_timeFor testMatchers:
by_equalityby_regexby_dateby_timestampby_timeby_type
minOccurrence and maxOccurrence.by_commandby_nullConsider the following example:
Groovy DSL. +
Contract contractDsl = Contract.make {
request {
method 'GET'
urlPath '/get'
@@ -764,7 +1143,7 @@ following, depending on the JSON path:'complex.key' : 'foo'
]
])
- stubMatchers {
+ bodyMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
@@ -781,11 +1160,15 @@ following, depending on the JSON path:200
+ 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",
@@ -805,9 +1188,10 @@ following, depending on the JSON path:'complex.key' : 'foo'
- ]
+ ],
+ nullValue: null
])
- testMatchers {
+ bodyMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}"))
// asserts the jsonpath value against the provided value
@@ -816,6 +1200,10 @@ following, depending on the JSON path:'$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
+ jsonPath('$.positiveInteger', byRegex(anInteger()))
+ jsonPath('$.negativeInteger', byRegex(anInteger()))
+ jsonPath('$.positiveDecimalNumber', byRegex(aDouble()))
+ jsonPath('$.negativeDecimalNumber', byRegex(aDouble()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
// asserts vs inbuilt time related regex
jsonPath('$.date', byDate())
@@ -847,17 +1235,152 @@ following, depending on the JSON path:// 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}'))))
}
}
-}In the preceding example, you can see the dynamic portions of the contract in the
+}
+
YAML.
+
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'
+ nullValue: null
+ matchers:
+ 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
+ headers:
+ Content-Type: application/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
+ valueWithMinEmpty: []
+ valueWithMaxEmpty: []
+ key:
+ 'complex.key' : 'foo'
+ nulValue: null
+ matchers:
+ 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: $.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
+
In the preceding example, you can see the dynamic portions of the contract in the
matchers sections. For the request part, you can see that, for all fields but
valueWithoutAMatcher, the values of the regular expressions that the stub should
contain are explicitly set. For the valueWithoutAMatcher, the verification takes place
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
+equality check.
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 +1391,7 @@ between the min and maximum occurrence.
The resulting test wou
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,83 +1402,84 @@ separates the autogenerated assertions and the assertion from matchers):"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);
- assertThatValueIsANumber(parsedJson.read("$.duck"));
![[Important]](images/important.png)
Important Notice that, for the byCommand method, the example calls the
+ 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]](images/important.png)
Important Notice that, for the byCommand method, the example calls the
assertThatValueIsANumber. This method must be defined in the test base class or be
statically imported to your tests. Notice that the byCommand call was converted to
assertThatValueIsANumber(parsedJson.read("$.duck"));. That means that the engine took
the method name and passed the proper JSON path as a parameter to it.
The resulting WireMock stub is in the following example:
'''
{
"request" : {
- "urlPath" : "/get",
- "method" : "POST",
- "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 =~ /(.*)/)]"
- } ]
+ "urlPath" : "/get",
+ "method" : "POST",
+ "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+|\\\\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" : "{\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"number\\":123,\\"aBoolean\\":true,\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"valueWithMin\\":[1,2,3],\\"time\\":\\"01:02:34\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithoutAMatcher\\":\\"foo\\"}",
- "headers" : {
- "Content-Type" : "application/json"
- }
+ "status" : 200,
+ "body" : "{\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"number\\":123,\\"aBoolean\\":true,\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"valueWithMin\\":[1,2,3],\\"time\\":\\"01:02:34\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithoutAMatcher\\":\\"foo\\"}",
+ "headers" : {
+ "Content-Type" : "application/json"
+ }
}
}
-'''
![[Important]](images/important.png)
Important If you use a matcher, then the part of the request aned response that the
+'''
![[Important]](images/important.png)
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.
Consider the following example:
Contract.make {
@@ -964,7 +1488,7 @@ collection.
Consider the following example:
"/foo")
}
response {
- status 200
+ status OK()
body(events: [[
operation : 'EXPORT',
eventId : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
@@ -976,7 +1500,7 @@ collection.
Consider the following example:
'$.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('.+'))
@@ -1021,17 +1545,22 @@ content type set. Otherwise, the default of application/oc
assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
'''
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:
org.springframework.cloud.contract.spec.Contract.make {
+provide an async() method in the response section. The following code shows an example:Groovy DSL.
+
org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url '/get'
}
response {
- status 200
+ status OK()
body 'Passed'
async()
}
-}Spring Cloud Contract supports context paths.
![[Important]](images/important.png)
Important The only change needed to fully support context paths is the switch on the
+}
+
YAML.
+
response:
+ async: true
+
Spring Cloud Contract supports context paths.
![[Important]](images/important.png)
Important The only change needed to fully support context paths is the switch on the
PRODUCER side. Also, the autogenerated tests must use EXPLICIT mode. The consumer
side remains untouched. In order for the generated test to pass, you must use EXPLICIT
mode.
Maven.
@@ -1056,7 +1585,7 @@ socket.
Consider the following contract:
or
url '/my-context-path/url'
}
response {
- status 200
+ status OK()
}
}
The following example shows how to set up a base class and Rest Assured:
import io.restassured.RestAssured;
import org.junit.Before;
@@ -1075,9 +1604,36 @@ socket.Consider the following contract:
or
}
}
If you do it this way:
- All of your requests in the autogenerated tests are sent to the real endpoint with your
context path included (for example,
/my-context-path/url). - Your contracts reflect that you have a context path. Your generated stubs also have
-that information (for example, in the stubs, you have to call
/my-context-path/url).
The DSL for messaging looks a little bit different than the one that focuses on HTTP. The
-following sections explain the differences:
The output message can be triggered by calling a method (such as a Scheduler when a was
-started and a message was sent), as shown in the following example:
def dsl = Contract.make {
+that information (for example, in the stubs, you have to call /my-context-path/url).Spring Cloud Contract requires the usage of EXPLICIT mode in your generated tests
+to work with Web Flux.
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'
+}
+
The following example shows how to set up a base class and Rest Assured for Web Flux:
@RunWith(SpringRunner.class)
+@SpringBootTest(classes = BeerRestBase.Config.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties = "server.port=0")
+public abstract class BeerRestBase {
+
+ // your tests go here
+
+ // in this config class you define all controllers and mocked services
+ include::{samples_url}/producer_webflux/src/test/java/com/example/BeerRestBase.java[tags=config,indent=0]
+
+}
The DSL for messaging looks a little bit different than the one that focuses on HTTP. The
+following sections explain the differences:
The output message can be triggered by calling a method (such as a Scheduler when a was
+started and a message was sent), as shown in the following example:
Groovy DSL.
+
def dsl = Contract.make {
// Human readable description
description 'Some description'
// Label by means of which the output message can be triggered
@@ -1098,11 +1654,31 @@ started and a message was sent), as shown in the following example:'BOOK-NAME', 'foo')
}
}
-}In the previous example case, the output message is sent to output if a method called
+}
+
YAML.
+
# Human readable description
+description: Some description
+# Label by means of which the output message can be triggered
+label: some_label
+input:
+ # the contract will be triggered by a method
+ triggeredBy: bookReturnedTriggered()
+# output message of the contract
+outputMessage:
+ # destination to which the output message will be sent
+ sentTo: output
+ # the body of the output message
+ body:
+ bookName: foo
+ # the headers of the output message
+ headers:
+ BOOK-NAME: foo
+
In the previous example case, the output message is sent to output if a method called
bookReturnedTriggered is executed. On the message publisher’s side, we generate a
test that calls that method to trigger the message. On the consumer side, you can use
-the some_label to trigger the message.
The output message can be triggered by receiving a message, as shown in the following
-example:
def dsl = Contract.make {
+the some_label to trigger the message.The output message can be triggered by receiving a message, as shown in the following
+example:
Groovy DSL.
+
def dsl = Contract.make {
description 'Some Description'
label 'some_label'
// input is a message
@@ -1127,11 +1703,36 @@ example:def dsl = Contract.make {
header('BOOK-NAME', 'foo')
}
}
-}In the preceding example, the output message is sent to output if a proper message is
+}
+
YAML.
+
# Human readable description
+description: Some description
+# Label by means of which the output message can be triggered
+label: some_label
+# input is a message
+input:
+ messageFrom: input
+ # has the following body
+ messageBody:
+ bookName: 'foo'
+ # and the following headers
+ messageHeaders:
+ sample: 'header'
+# output message of the contract
+outputMessage:
+ # destination to which the output message will be sent
+ sentTo: output
+ # the body of the output message
+ body:
+ bookName: foo
+ # the headers of the output message
+ headers:
+ BOOK-NAME: foo
+
In the preceding example, the output message is sent to output if a proper message is
received on the input destination. On the message publisher’s side, the engine
generates a test that sends the input message to the defined destination. On the
consumer side, you can either send a message to the input destination or use a label
-(some_label in the example) to trigger the message.
In HTTP, you have a notion of client/stub and `server/test notation. You can also
+(some_label in the example) to trigger the message.
![[Important]](images/important.png)
Important This section is valid only for Groovy DSL.
In HTTP, you have a notion of client/stub and `server/test notation. You can also
use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also
provides the consumer and producer methods, as presented in the following example
(note that you can use either $ or value methods to provide consumer and producer
@@ -1152,11 +1753,12 @@ parts):
Contract.make {
bookName: 'foo'
])
}
-}You can define multiple contracts in one file. Such a contract might resemble the
-following example:
import org.springframework.cloud.contract.spec.Contract
+base class or in a static import. Spring Cloud Contract will execute that method
+in the generated test.You can define multiple contracts in one file. Such a contract might resemble the
+following example:
Groovy DSL.
+
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
@@ -1166,7 +1768,7 @@ following example:'/users/1')
}
response {
- status 200
+ status OK()
}
},
Contract.make {
@@ -1175,10 +1777,26 @@ following example:'/users/2')
}
response {
- status 200
+ status OK()
}
}
-]
In the preceding example, one contract has the name field and the other does not. This
+]
+
YAML.
+
---
+name: should post a user
+request:
+ method: POST
+ url: /users/1
+response:
+ status: 200
+
+---
+request:
+ method: POST
+ url: /users/2
+response:
+ status: 200
+
In the preceding example, one contract has the name field and the other does not. This
leads to generation of two tests that look more or less like this:
package org.springframework.cloud.contract.verifier.tests.com.hello;
import com.example.TestBase;
@@ -1226,5 +1844,93 @@ leads to generation of two tests that look more or less like this:The generated stubs is shown in the following example:
should post a user.json
1_WithList.json
As you can see, the first file got the name parameter from the contract. The second
got the name of the contract file (WithList.groovy) prefixed with the index (in this
-case, the contract had an index of 1 in the list of contracts in the file).
![[Tip]](images/tip.png)
Tip As you can see, it iss much better if you name your contracts because doing so makes
-your tests far more meaningful.
\ No newline at end of file
+case, the contract had an index of 1 in the list of contracts in the file).![[Tip]](images/tip.png)
Tip As you can see, it is much better if you name your contracts because doing so makes
+your tests far more meaningful.
When you want to include the requests and responses of your API using Spring REST Docs,
+you only need to make some minor changes to your setup if you are using MockMvc and RestAssuredMockMvc.
+Simply include the following dependencies if you haven’t already.
Maven.
+
<dependency>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-starter-contract-verifier</artifactId>
+ <scope>test</scope>
+</dependency>
+<dependency>
+ <groupId>org.springframework.restdocs</groupId>
+ <artifactId>spring-restdocs-mockmvc</artifactId>
+ <optional>true</optional>
+</dependency>
+
Gradle.
+
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
+testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'
+
Next you need to make some changes to your base class like the following example.
package com.example.fraud;
+
+import io.restassured.module.mockmvc.RestAssuredMockMvc;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = Application.class)
+public abstract class FraudBaseWithWebAppSetup {
+
+ private static final String OUTPUT = "target/generated-snippets";
+
+ @Rule
+ public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
+
+ @Rule public TestName testName = new TestName();
+
+ @Autowired
+ private WebApplicationContext context;
+
+ @Before
+ public void setup() {
+ RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context)
+ .apply(documentationConfiguration(this.restDocumentation))
+ .alwaysDo(document(getClass().getSimpleName() + "_" + testName.getMethodName()))
+ .build());
+ }
+
+ protected void assertThatRejectionReasonIsNull(Object rejectionReason) {
+ assert rejectionReason == null;
+ }
+}
In case you are using the standalone setup, you can set up RestAssuredMockMvc like this:
package com.example.fraud;
+
+import io.restassured.module.mockmvc.RestAssuredMockMvc;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+
+public abstract class FraudBaseWithStandaloneSetup {
+
+ private static final String OUTPUT = "target/generated-snippets";
+
+ @Rule
+ public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
+
+ @Rule public TestName testName = new TestName();
+
+ @Before
+ public void setup() {
+ RestAssuredMockMvc.standaloneSetup(MockMvcBuilders.standaloneSetup(new FraudDetectionController())
+ .apply(documentationConfiguration(this.restDocumentation))
+ .alwaysDo(document(getClass().getSimpleName() + "_" + testName.getMethodName())));
+ }
+
+}
![[Tip]](images/tip.png)
Tip You don’t need to specify the output directory for the generated snippets since version 1.2.0.RELEASE of Spring REST Docs.