![]() | Important |
|---|---|
Remember that, inside the contract file, you have to provide the fully
qualified name to the |
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.
![]() | Tip |
|---|---|
Spring Cloud Contract supports defining multiple contracts in a single file. |
The Contract is present in the spring-cloud-contract-spec module of the
Spring
Cloud Contract Verifier repository.
The following is a complete example of a contract definition:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/api/12'
headers {
header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json'
}
body '''\
[{
"created_at": "Sat Jul 26 09:38:57 +0000 2014",
"id": 492967299297845248,
"id_str": "492967299297845248",
"text": "Gonna see you at Warsaw",
"place":
{
"attributes":{},
"bounding_box":
{
"coordinates":
[[
[-77.119759,38.791645],
[-76.909393,38.791645],
[-76.909393,38.995548],
[-77.119759,38.995548]
]],
"type":"Polygon"
},
"country":"United States",
"country_code":"US",
"full_name":"Washington, DC",
"id":"01fbe706f872cb32",
"name":"Washington",
"place_type":"city",
"url": "https://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
}
}]
'''
}
response {
status 200
}
}![]() | 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 help us implement this feature. |
![]() | Warning |
|---|---|
The support for verifying the size of JSON arrays is experimental. If you want
to turn it on, please set the value of the following system property to |
![]() | Warning |
|---|---|
Because JSON structure can have any form, it can be impossible to parse it
properly when using the |
The following sections describe the most common top-level elements:
You can add a description to your contract. The description is arbitrary text. The
following code shows an example:
org.springframework.cloud.contract.spec.Contract.make {
description('''
given:
An input
when:
Sth happens
then:
Output
''')
}You can provide a name for your contract. Assume that you provided the following name:
should register a user. If you do so, the name of the autogenerated test is
validate_should_register_a_user. Also, the name of the stub in a WireMock stub is
should_register_a_user.json.
![]() | Important |
|---|---|
You must ensure that the name does not contain any characters that make the generated test not compile. Also, remember that, if you provide the same name for multiple contracts, your autogenerated tests fail to compile and your generated stubs override each other. |
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:
org.springframework.cloud.contract.spec.Contract.make {
ignored()
}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 {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
request {
//...
}
// Definition of HTTP response part of the contract
// (a service implementing this contract should respond
// with following response after receiving request
// specified in "request" part above).
response {
//...
}
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
priority 1
}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 {
request {
// HTTP request method (GET/POST/PUT/DELETE).
method 'GET'
// Path component of request URL is specified as follows.
urlPath('/users')
}
response {
//...
}
}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 {
request {
method 'GET'
// Specifying `url` and `urlPath` in one contract is illegal.
url('http://localhost:8888/users')
}
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 {
request {
//...
urlPath('/users') {
// Each parameter is specified in form
// `'paramName' : paramValue` where parameter value
// may be a simple literal or one of matcher functions,
// all of which are used in this example.
queryParameters {
// If a simple literal is used as value
// default matcher function is used (equalTo)
parameter 'limit': 100
// `equalTo` function simply compares passed value
// using identity operator (==).
parameter 'filter': equalTo("email")
// `containing` function matches strings
// that contains passed substring.
parameter 'gender': value(consumer(containing("[mf]")), producer('mf'))
// `matching` function tests parameter
// against passed regular expression.
parameter 'offset': value(consumer(matching("[0-9]+")), producer(123))
// `notMatching` functions tests if parameter
// does not match passed regular expression.
parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3))
}
}
//...
}
response {
//...
}
}request may contain additional request headers, as shown in the following example:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
headers {
header 'key': 'value'
contentType(applicationJson())
}
//...
}
response {
//...
}
}request may contain a request body, as shown in the following example:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
// Currently only JSON format of request body is supported.
// Format will be determined from a header or body's content.
body '''{ "login" : "john", "name": "John The Contract" }'''
}
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 {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
// key (parameter name), value (parameter value) pair
formParameter: $(c(regex('".+"')), p('"formParameterValue"')),
someBooleanParameter: $(c(regex(anyBoolean())), p('true')),
// a named parameter (e.g. with `file` name) that represents file with
// `name` and `content`. You can also call `named("fileName", "fileContent")`
file: named(
// name of the file
name: $(c(regex(nonEmpty())), p('filename.csv')),
// content of the file
content: $(c(regex(nonEmpty())), p('file content')))
)
}
response {
status 200
}
}In the preceding example, we define parameters in either of two ways:
formParameter: $(consumer(…), producer(…))).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: MockMvcRequestSpecification request = given() .header("Content-Type", "multipart/form-data;boundary=AaB03x") .param("formParameter", "\"formParameterValue\"") .param("someBooleanParameter", "true") .multiPart("file", "filename.csv", "file content".getBytes()); // when: ResponseOptions response = given().spec(request) .put("/multipart"); // then: assertThat(response.statusCode()).isEqualTo(200);
The WireMock stub is as follows:
''' { "request" : { "url" : "/multipart", "method" : "PUT", "headers" : { "Content-Type" : { "matches" : "multipart/form-data;boundary=AaB03x.*" } }, "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=\\"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=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n[\\\\S\\\\s]+\\r\\n--\\\\1.*" } ] }, "response" : { "status" : 200, "transformers" : [ "response-template" ] } } '''
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 {
request {
//...
}
response {
// Status code sent by the server
// in response to request specified above.
status 200
}
}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
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
the Groovy map notation, with $(). The following example shows how to set dynamic
properties with the value method:
value(consumer(...), producer(...)) value(c(...), p(...)) value(stub(...), test(...)) value(client(...), server(...))
The following example shows how to set dynamic properties with $():
$(consumer(...), producer(...)) $(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 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 {
request {
method('GET')
url $(consumer(~/\/[0-9]{2}/), producer('/12'))
}
response {
status 200
body(
id: $(anyNumber()),
surname: $(
consumer('Kowalsky'),
producer(regex('[a-zA-Z]+'))
),
name: 'Jan',
created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))),
correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
producer(regex('[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}'))
)
)
headers {
header 'Content-Type': 'text/plain'
}
}
}You can also provide only one side of the communication with a regular expression. If you do so, then the contract engine automatically provides the generated string that matches the provided regular expression. The following code shows an example:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url value(consumer(regex('/foo/[0-9]{5}')))
body([
requestElement: $(consumer(regex('[0-9]{5}')))
])
headers {
header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*'))))
}
}
response {
status 200
body([
responseElement: $(producer(regex('[0-9]{7}')))
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}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 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 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 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])') protected static final Pattern ANY_TIME = Pattern.compile('(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])') protected static final Pattern NON_EMPTY = Pattern.compile(/[\S\s]+/) protected static final Pattern NON_BLANK = Pattern.compile(/^\s*\S[\S\s]*/) protected static final Pattern ISO8601_WITH_OFFSET = 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])(\.\d{3})?(Z|[+-][01]\d:[0-5]\d)/) protected static Pattern anyOf(String... values){ return Pattern.compile(values.collect({"^$it\$"}).join("|")) } String onlyAlphaUnicode() { return ONLY_ALPHA_UNICODE.pattern() } String number() { return NUMBER.pattern() } String anyBoolean() { return TRUE_OR_FALSE.pattern() } String ipAddress() { return IP_ADDRESS.pattern() } String hostname() { return HOSTNAME_PATTERN.pattern() } String email() { return EMAIL.pattern() } String url() { return URL.pattern() } String uuid(){ return UUID.pattern() } String isoDate() { return ANY_DATE.pattern() } String isoDateTime() { return ANY_DATE_TIME.pattern() } String isoTime() { return ANY_TIME.pattern() } String iso8601WithOffset() { return ISO8601_WITH_OFFSET.pattern() } String nonEmpty() { return NON_EMPTY.pattern() } String nonBlank() { return NON_BLANK.pattern() }
In your contract, you can use it as shown in the following example:
Contract dslWithOptionalsInString = Contract.make {
priority 1
request {
method POST()
url '/users/password'
headers {
contentType(applicationJson())
}
body(
email: $(consumer(optional(regex(email()))), producer('[email protected]')),
callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
)
}
response {
status 404
headers {
contentType(applicationJson())
}
body(
code: value(consumer("123123"), producer(optional("123123"))),
message: "User not found by email = [${value(producer(regex(email())), consumer('[email protected]'))}]"
)
}
}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 {
method 'POST'
url '/users/password'
headers {
contentType(applicationJson())
}
body(
email: $(consumer(optional(regex(email()))), producer('[email protected]')),
callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
)
}
response {
status 404
headers {
header 'Content-Type': 'application/json'
}
body(
code: value(consumer("123123"), producer(optional("123123")))
)
}
}By wrapping a part of the body with the optional() method, you create a regular
expression that must be present 0 or more times.
If you use Spock for, the following test would be generated from the previous example:
""" given: def request = given() .header("Content-Type", "application/json") .body('''{"email":"[email protected]","callback_url":"http://partners.com"}''') when: def response = given().spec(request) .post("/users/password") then: response.statusCode == 404 response.header('Content-Type') == 'application/json' and: DocumentContext parsedJson = JsonPath.parse(response.body.asString()) assertThatJson(parsedJson).field("['code']").matches("(123123)?") """
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" } } }, "response" : { "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 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 {
method 'PUT'
url $(consumer(regex('^/api/[0-9]{2}$')), producer('/api/12'))
headers {
header 'Content-Type': 'application/json'
}
body '''\
[{
"text": "Gonna see you at Warsaw"
}]
'''
}
response {
body (
path: $(consumer('/api/12'), producer(regex('^/api/[0-9]{2}$'))),
correlationId: $(consumer('1223456'), producer(execute('isProperCorrelationId($it)')))
)
status 200
}
}The following code shows the base class portion of the test case:
abstract class BaseMockMvcSpec extends Specification { def setup() { RestAssuredMockMvc.standaloneSetup(new PairIdController()) } void isProperCorrelationId(Integer correlationId) { assert correlationId == 123456 } void isEmpty(String value) { assert value == null } }
![]() | Important |
|---|---|
You cannot use both a String and |
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 in the JSON.JSONArray: If you point to a List in the JSON.Map: If you point to a Map in the JSON.Number: If you point to Integer, Double etc. in the JSON.Boolean: If you point to a Boolean in the JSON.In the request part of the contract, you can specify that the body should be taken from
a method.
![]() | Important |
|---|---|
You must provide both the consumer and the producer side. The |
The following example shows how to read an object from JSON:
Contract contractDsl = Contract.make {
request {
method 'GET'
url '/something'
body(
$(c("foo"), p(execute("hashCode()")))
)
}
response {
status 200
}
}The preceding example results in calling the hashCode() method in the request body.
It should resemble the following code:
// given: MockMvcRequestSpecification request = given() .body(hashCode()); // when: ResponseOptions response = given().spec(request) .get("/something"); // 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
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 {
request {
method 'GET'
url('/api/v1/xxxx') {
queryParameters {
parameter("foo", "bar")
parameter("foo", "bar2")
}
}
headers {
header(authorization(), "secret")
header(authorization(), "secret2")
}
body(foo: "bar", baz: 5)
}
response {
status 200
headers {
header(authorization(), "foo ${fromRequest().header(authorization())} bar")
}
body(
url: fromRequest().url(),
param: fromRequest().query("foo"),
paramIndex: fromRequest().query("foo", 1),
authorization: fromRequest().header("Authorization"),
authorization2: fromRequest().header("Authorization", 1),
fullBody: fromRequest().body(),
responseFoo: fromRequest().body('$.foo'),
responseBaz: fromRequest().body('$.baz'),
responseBaz2: "Bla bla ${fromRequest().body('$.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") .body("{\"foo\":\"bar\",\"baz\":5}"); // when: ResponseOptions response = given().spec(request) .queryParam("foo","bar") .queryParam("foo","bar2") .get("/api/v1/xxxx"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Authorization")).isEqualTo("foo secret bar"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}"); assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret"); assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2"); assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx"); assertThatJson(parsedJson).field("['param']").isEqualTo("bar"); assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2"); assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1"); assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5); assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar"); assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2"); assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla");
As you can see, elements from the request have been properly referenced in the response.
The generated WireMock stub should resemble the following example:
{ "request" : { "urlPath" : "/api/v1/xxxx", "method" : "POST", "headers" : { "Authorization" : { "equalTo" : "secret2" } }, "queryParameters" : { "foo" : { "equalTo" : "bar2" } }, "bodyPatterns" : [ { "matchesJsonPath" : "$[?(@.['baz'] == 5)]" }, { "matchesJsonPath" : "$[?(@.['foo'] == 'bar')]" } ] }, "response" : { "status" : 200, "body" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}", "headers" : { "Authorization" : "{{{request.headers.Authorization.[0]}}};foo" }, "transformers" : [ "response-template" ] } }
Sending a request such as the one presented in the request part of the contract results
in sending the following response body:
{ "url" : "/api/v1/xxxx?foo=bar&foo=bar2", "path" : "/api/v1/xxxx", "pathIndex" : "v1", "param" : "bar", "paramIndex" : "bar2", "authorization" : "secret", "authorization2" : "secret2", "fullBody" : "{\"foo\":\"bar\",\"baz\":5}", "responseFoo" : "bar", "responseBaz" : 5, "responseBaz2" : "Bla bla bar bla bla" }
![]() | Important |
|---|---|
This feature works only with WireMock having a version greater than or equal
to 2.5.1. The Spring Cloud Contract Verifier uses WireMock’s
|
escapejsonbody: Escapes the request body in a format that can be embedded in a JSON.jsonpath: For a given parameter, find an object in the request body.WireMock lets you register custom extensions. By default, Spring Cloud Contract registers
the transformer, which lets you reference a request from a response. If you want to
provide your own extensions, you can register an implementation of the
org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions interface.
Since we use the spring.factories extension approach, you can create an entry in
META-INF/spring.factories file similar to the following:
Unresolved directive in verifier_contract.adoc - include::../../../../spring-cloud-contract-stub-runner/src/test/resources/META-INF/spring.factories[indent=0]The following is an example of a custom extension:
TestWireMockExtensions.groovy.
Unresolved directive in verifier_contract.adoc - include::../../../../spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/TestWireMockExtensions.groovy[indent=0]
![]() | Important |
|---|---|
Remember to override the |
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
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
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
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 {
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'
]
])
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())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
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
],
valueWithMinEmpty: [],
valueWithMaxEmpty: [],
key: [
'complex.key' : 'foo'
]
])
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)
})
jsonPath('$.valueWithMinEmpty', byType {
// results in verification of size of array (min 0)
minOccurrence(0)
})
jsonPath('$.valueWithMaxEmpty', byType {
// results in verification of size of array (max 0)
maxOccurrence(0)
})
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
}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
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
defined in the response body, and passes the following check (based on the method being called):
$.valueWithTypeMatch, the engine checks whether the type is the same.$.valueWithMin, the engine check the type and asserts whether the size is greater
than or equal to the minimum occurrence.$.valueWithMax, the engine checks the type and asserts whether the size is
smaller than or equal to the maximum occurrence.$.valueWithMinMax, the engine checks the type and asserts whether the size is
between the min and maximum occurrence.The resulting test would resemble the following example (note that an and section
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\"}"); // 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((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).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((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class); assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).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((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 |
|---|---|
Notice that, for the |
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 =~ /(.*)/)]" } ] }, "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" } } } '''
![]() | Important |
|---|---|
If you use a |
Consider the following example:
Contract.make {
request {
method 'GET'
url("/foo")
}
response {
status 200
body(events: [[
operation : 'EXPORT',
eventId : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
status : 'OK'
], [
operation : 'INPUT_PROCESSING',
eventId : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
status : 'OK'
]
]
)
testMatchers {
jsonPath('$.events[0].operation', byRegex('.+'))
jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
jsonPath('$.events[0].status', byRegex('.+'))
}
}
}The preceding code leads to creating the following test (the code block shows only the assertion section):
and: DocumentContext parsedJson = JsonPath.parse(response.body.asString()) assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99") assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT") assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING") assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a") assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK") and: assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+") assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\$") assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+")
As you can see, the assertion is malformed. Only the first element of the array got
asserted. In order to fix this, you should apply the assertion to the whole $.events
collection and assert it with the byCommand(…) method.
The Spring Cloud Contract Verifier supports the JAX-RS 2 Client API. The base class needs
to define protected WebTarget webTarget and server initialization. The only option for
testing JAX-RS API is to start a web server. Also, a request with a body needs to have a
content type set. Otherwise, the default of application/octet-stream gets used.
In order to use JAX-RS mode, use the following settings:
testMode == 'JAXRSCLIENT'The following example shows a generated test API:
''' // when: Response response = webTarget .path("/users") .queryParam("limit", "10") .queryParam("offset", "20") .queryParam("filter", "email") .queryParam("sort", "name") .queryParam("search", "55") .queryParam("age", "99") .queryParam("name", "Denis.Stepanov") .queryParam("email", "[email protected]") .request() .method("GET"); String responseAsString = response.readEntity(String.class); // then: assertThat(response.getStatus()).isEqualTo(200); // and: DocumentContext parsedJson = JsonPath.parse(responseAsString); 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 {
request {
method GET()
url '/get'
}
response {
status 200
body 'Passed'
async()
}
}Spring Cloud Contract supports context paths.
![]() | 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.
<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 generate a test that DOES NOT use MockMvc. It means that you generate real requests and you need to setup your generated test’s base class to work on a real socket.
Consider the following contract:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/my-context-path/url'
}
response {
status 200
}
}The following example shows how to set up a base class and Rest Assured:
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; } }
If you do it this way:
/my-context-path/url)./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 {
// Human readable description
description 'Some description'
// Label by means of which the output message can be triggered
label 'some_label'
// input to the contract
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 {
header('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 {
description 'Some Description'
label 'some_label'
// input is a message
input {
// the message was received from this destination
messageFrom('input')
// has the following body
messageBody([
bookName: 'foo'
])
// and the following headers
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output')
body([
bookName: 'foo'
])
headers {
header('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
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
parts):
Contract.make {
label 'some_label'
input {
messageFrom value(consumer('jms:output'), producer('jms:input'))
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo $(consumer('jms:input'), producer('jms:output'))
body([
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 [ Contract.make { name("should post a user") request { method 'POST' url('/users/1') } response { status 200 } }, Contract.make { 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; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification; import com.jayway.restassured.response.ResponseOptions; import org.junit.Test; import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*; import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; public class V1Test extends TestBase { @Test public void validate_should_post_a_user() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .post("/users/1"); // then: assertThat(response.statusCode()).isEqualTo(200); } @Test public void validate_withList_1() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .post("/users/2"); // then: assertThat(response.statusCode()).isEqualTo(200); } }
Notice that, for the contract that has the name field, the generated test method is named
validate_should_post_a_user. For the one that does not have the name, it is called
validate_withList_1. It corresponds to the name of the file WithList.groovy and the
index of the contract in the list.
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 |
|---|---|
As you can see, it iss much better if you name your contracts because doing so makes your tests far more meaningful. |