Contract DSL
Spring Cloud Contract supports out of the box 2 types of DSL. One written in
Groovy and one written in YAML.
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.
Remember that, inside the Groovy contract file, you have to provide the fully
qualified name to the Contract class and make static imports, such as
org.springframework.cloud.spec.Contract.make { … }. You can also provide an import to
the Contract class: import org.springframework.cloud.spec.Contract and then call
Contract.make { … }.
|
| Spring Cloud Contract supports defining multiple contracts in a single file. |
The following is a complete example of a Groovy contract definition:
The following is a complete example of a YAML contract definition:
You can compile contracts to stubs mapping using standalone maven command:
mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert
|
Limitations
| Spring Cloud Contract Verifier does not properly support XML. Please use JSON or help us implement this feature. |
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 true:
spring.cloud.contract.verifier.assert.size. By default, this feature is set to false.
You can also provide the assertJsonSize property in the plugin configuration.
|
Because JSON structure can have any form, it can be impossible to parse it
properly when using the Groovy DSL and the value(consumer(…), producer(…)) notation in GString. That
is why you should use the Groovy Map notation.
|
Common Top-Level elements
The following sections describe the most common top-level elements:
Description
You can add a description to your contract. The description is arbitrary text. The
following code shows an example:
Name
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.
| 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. |
Ignoring Contracts
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:
Passing Values from Files
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.json
Further assume that your contract is as follows:
Further assume that the JSON files is as follows:
request.json
response.json
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.
If you need to pass the contents of a file in a binary form
it’s enough for you to use the fileAsBytes method in Groovy DSL or bodyFromFileAsBytes field in YAML.
| You should use this approach whenever you want to work with binary payloads both for HTTP and messaging. |
HTTP Top-Level Elements
The following methods can be called in the top-level closure of a contract definition.
request and response are mandatory. priority is optional.
...
...
If you want to make your contract have a higher value of priority
you need to pass a lower number to the priority tag / method. E.g. priority with
value 5 has higher priority than priority with value 10.
|
Request
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.
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.
request may contain query parameters.
...
request may contain additional request headers, as shown in the following example:
...
request may contain additional request cookies, as shown in the following example:
...
request may contain a request body:
...
request may contain multipart elements. To include multipart elements, use the
multipart method/section, as shown in the following examples
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
formParameter: $(consumer(…), producer(…))). -
By using the
named(…)method that lets you set a named parameter. A named parameter can set anameandcontent. You can call it either via a method with two arguments, such asnamed("fileName", "fileContent"), or via a map notation, such asnamed(name: "fileName", content: "fileContent").
-
The multipart parameters are set via
multipart.paramssection -
The named parameters (the
fileNameandfileContentfor a given parameter name) can be set via themultipart.namedsection. That section contains theparamName(name of the parameter),fileName(name of the file),fileContent(content of the file) fields -
The dynamic bits can be set via the
matchers.multipartsection-
for parameters use the
paramssection that can acceptregexor apredefinedregular expression -
for named params use the
namedsection where first you define the parameter name viaparamNameand then you can pass the parametrization of eitherfileNameorfileContentviaregexor apredefinedregular 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\"")
.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:
Response
The response must contain an HTTP status code and may contain other information. The following code shows an example:
...
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).
Via the Groovy DSL you can reference the org.springframework.cloud.contract.spec.internal.HttpStatus
methods to provide a meaningful status instead of a digit. E.g. you can call
OK() for a status 200 or BAD_REQUEST() for 400.
|
Dynamic properties
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.
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.
Before 2.0.0 these were set using testMatchers and stubMatchers,
check out the migration guide for more information.
|
For YAML you can only use the matchers section.
Dynamic properties inside the body
| This section is valid only for Groovy DSL. Check out the 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(...))
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.
Regular expressions
| This section is valid only for Groovy DSL. Check out the 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.
Make sure that regex matches a whole region of a sequence as internally a call to
Pattern.matches()
is called. For instance, abc pattern doesn’t match aabc string but .abc does.
There are several additional known limitations as well.
The following example shows how to use regular expressions to write a request:
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:
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:
In your contract, you can use it as shown in the following example:
To make matters even simpler you can use a set of predefined objects that will automatically assume that you want a regular expression to be passed.
All of those methods start with any prefix:
and this is an example of how you can reference those methods:
Limitations
Due to certain limitations of Xeger library that generates string out of
regex, do not use $ and ^ signs in your regex if you rely on automatic
generation. Issue 899
|
Do not use LocalDate instance as a value for $ like this $(consumer(LocalDate.now())).
It causes java.lang.StackOverflowError. Use $(consumer(LocalDate.now().toString())) instead.
Issue 900
|
Passing Optional Parameters
| This section is valid only for Groovy DSL. Check out the 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:
-
STUB side of the Request
-
TEST side of the Response
The following example shows how to provide optional parameters:
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:
The following stub would also be generated:
Executing Custom Methods on the Server Side
| This section is valid only for Groovy DSL. Check out the 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:
The following code shows the base class portion of the test case:
You cannot use both a String and execute to perform concatenation. For
example, calling header('Authorization', 'Bearer ' + execute('authToken()')) leads to
improper results. Instead, call header('Authorization', execute('authToken()')) and
ensure that the authToken() method returns everything you need.
|
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 aStringvalue in the JSON. -
JSONArray: If you point to aListin the JSON. -
Map: If you point to aMapin the JSON. -
Number: If you point toInteger,Doubleetc. in the JSON. -
Boolean: If you point to aBooleanin the JSON.
In the request part of the contract, you can specify that the body should be taken from
a method.
You must provide both the consumer and the producer side. The execute part
is applied for the whole body - not for parts of it.
|
The following example shows how to read an object from JSON:
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);
Referencing the Request from the Response
The best situation is to provide fixed values, but sometimes you need to reference a 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.
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 keyfoo, 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:
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"
}
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
response-template response transformer. It uses Handlebars to convert the Mustache {{{ }}} templates into
proper values. Additionally, it registers two helper functions:
|
-
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.
Registering Your Own WireMock Extension
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:
The following is an example of a custom extension:
Remember to override the applyGlobally() method and set it to false if you
want the transformation to be applied only for a mapping that explicitly requires it.
|
Dynamic Properties in the Matchers Sections
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 the bodyMatchers section for two reasons:
-
Define the dynamic values that should end up in a stub. You can set it in the
requestorinputMessagepart of your contract. -
Verify the result of your test. This section is present in the
responseoroutputMessageside of the contract.
Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the following matching possibilities:
-
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. You can also pass the type of the expected matched value (e.g.asString(),asLong()etc.) -
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.byTypecan take a closure, in which you can setminOccurrenceandmaxOccurrence. For the request side, you should use the closure to assert size of the collection. That way, you can assert the size of the flattened collection. To check the size of an unflattened collection, use a custom method with thebyCommand(…)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 afoomethod 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 aStringvalue. -
JSONArray: If you point to aList. -
Map: If you point to aMap. -
Number: If you point toInteger,Double, or other kind of number. -
Boolean: If you point to aBoolean.
-
-
byNull(): The value taken from the response via the provided JSON Path must be null
-
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
regexType: as_string
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_equality -
by_regex -
by_date -
by_timestamp -
by_time -
by_type-
there are 2 additional fields accepted:
minOccurrenceandmaxOccurrence.
-
-
-
For
testMatchers:-
by_equality -
by_regex -
by_date -
by_timestamp -
by_time -
by_type-
there are 2 additional fields accepted:
minOccurrenceandmaxOccurrence.
-
-
by_command -
by_null
-
You can also define which type the regular expression corresponds to via the regexType field. Below you can find the allowed list of regular expression types:
-
as_integer
-
as_double
-
as_float,
-
as_long
-
as_short
-
as_boolean
-
as_string
Consider the following example:
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 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
defined in the response body, and passes the following check (based on the method being called):
-
For
$.valueWithTypeMatch, the engine checks whether the type is the same. -
For
$.valueWithMin, the engine check the type and asserts whether the size is greater than or equal to the minimum occurrence. -
For
$.valueWithMax, the engine checks the type and asserts whether the size is smaller than or equal to the maximum occurrence. -
For
$.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\",\"key\":{\"complex.key\":\"foo\"}}");
// 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+|\\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)).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)).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)).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)).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)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
assertThatValueIsANumber(parsedJson.read("$.duck"));
assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");
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:
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 {
request {
method 'GET'
url("/foo")
}
response {
status OK()
body(events: [[
operation : 'EXPORT',
eventId : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
status : 'OK'
], [
operation : 'INPUT_PROCESSING',
eventId : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
status : 'OK'
]
]
)
bodyMatchers {
jsonPath('$.events[0].operation', byRegex('.+'))
jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
jsonPath('$.events[0].status', byRegex('.+'))
}
}
}
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.
JAX-RS Support
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:
Async Support
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 an async() 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 OK()
body 'Passed'
async()
}
}
response:
async: true
You can also use the fixedDelayMilliseconds method / property to add delay to your stubs.
org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url '/get'
}
response {
status 200
body 'Passed'
fixedDelayMilliseconds 1000
}
}
response:
fixedDelayMilliseconds: 1000
Working with Context Paths
Spring Cloud Contract supports context paths.
| 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. |
<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>
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:
The following example shows how to set up a base class and Rest Assured:
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).
Working with WebFlux
Spring Cloud Contract offers two ways of working with WebFlux.
WebFlux with WebTestClient
One of them is via the WebTestClient mode.
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>WEBTESTCLIENT</testMode>
</configuration>
</plugin>
contracts {
testMode = 'WEBTESTCLIENT'
}
The following example shows how to set up a WebTestClient base class and RestAssured
for WebFlux:
import io.restassured.module.webtestclient.RestAssuredWebTestClient;
import org.junit.Before;
public abstract class BeerRestBase {
@Before
public void setup() {
RestAssuredWebTestClient.standaloneSetup(
new ProducerController(personToCheck -> personToCheck.age >= 20));
}
}
}
WebFlux with Explicit mode
Another way is with the EXPLICIT mode in your generated tests
to work with WebFlux.
<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>
contracts {
testMode = 'EXPLICIT'
}
The following example shows how to set up a base class and Rest Assured for Web Flux:
// your tests go here
// in this config class you define all controllers and mocked services
}
XML Support for REST
For REST contracts, we also support XML request and response body.
The XML body has to be passed within the body element
as a String or GString. Also body matchers can be provided for
both request and response. In place of the jsonPath(…) method, the org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath
method should be used, with the desired xPath provided as the first argument
and the appropriate MatchingType as second. All the body matchers apart from byType() are supported.
Here is an example of a Groovy DSL contract with XML response body:
And below is an example of a YAML contract with XML request and response bodies:
include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml
Here is an example of an automatically generated test for XML response body:
@Test
public void validate_xmlMatches() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/xml");
// when:
ResponseOptions response = given().spec(request).get("/get");
// then:
assertThat(response.statusCode()).isEqualTo(200);
// and:
DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
.newDocumentBuilder();
Document parsedXml = documentBuilder.parse(new InputSource(
new StringReader(response.getBody().asString())));
// and:
assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc");
assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def");
assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}");
assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull();
assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p{L}]*");
assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo");
assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype");
}
Messaging Top-Level Elements
The DSL for messaging looks a little bit different than the one that focuses on HTTP. The following sections explain the differences:
Output Triggered by a Method
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:
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.
Output Triggered by a Message
The output message can be triggered by receiving a message, as shown in the following example:
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.
Consumer/Producer
| 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
parts):
Common
In the input or outputMessage section you can call assertThat with the name
of a method (e.g. assertThatMessageIsOnTheQueue()) that you have defined in the
base class or in a static import. Spring Cloud Contract will execute that method
in the generated test.
Multiple Contracts in One File
You can define multiple contracts in one file. Such a contract might resemble the following example:
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).
| As you can see, it is much better if you name your contracts because doing so makes your tests far more meaningful. |
Generating Spring REST Docs snippets from the contracts
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.
Next you need to make some changes to your base class like the following example.
In case you are using the standalone setup, you can set up RestAssuredMockMvc like this:
| You don’t need to specify the output directory for the generated snippets since version 1.2.0.RELEASE of Spring REST Docs. |
Customization
| This section is valid only for Groovy DSL |
You can customize the Spring Cloud Contract Verifier by extending the DSL, as shown in the remainder of this section.
Extending the DSL
You can provide your own functions to the DSL. The key requirement for this feature is to maintain the static compatibility. Later in this document, you can see examples of:
-
Creating a JAR with reusable classes.
-
Referencing of these classes in the DSLs.
You can find the full example here.
Common JAR
The following examples show three classes that can be reused in the DSLs.
PatternUtils contains functions used by both the consumer and the producer.
ConsumerUtils contains functions used by the consumer.
ProducerUtils contains functions used by the producer.
Adding the Dependency to the Project
In order for the plugins and IDE to be able to reference the common JAR classes, you need to pass the dependency to your project.
Test the Dependency in the Project’s Dependencies
First, add the common jar dependency as a test dependency. Because your contracts files are available on the test resources path, the common jar classes automatically become visible in your Groovy files. The following examples show how to test the dependency:
Test a Dependency in the Plugin’s Dependencies
Now, you must add the dependency for the plugin to reuse at runtime, as shown in the following example:
Referencing classes in DSLs
You can now reference your classes in your DSL, as shown in the following example:
You can set the Spring Cloud Contract plugin up by setting convertToYaml to true. That way you will NOT have to add the dependency with the extended functionality to the consumer side, since the consumer side will be using YAML contracts instead of Groovy ones.
|
Using the Pluggable Architecture
You may encounter cases where you have your contracts have been defined in other formats, such as YAML, RAML or PACT. In those cases, you still want to benefit from the automatic generation of tests and stubs. You can add your own implementation for generating both tests and stubs. Also, you can customize the way tests are generated (for example, you can generate tests for other languages) and the way stubs are generated (for example, you can generate stubs for other HTTP server implementations).
Custom Contract Converter
The ContractConverter interface lets you register your own implementation of a contract
structure converter. The following code listing shows the ContractConverter interface:
Your implementation must define the condition on which it should start the conversion. Also, you must define how to perform that conversion in both directions.
Once you create your implementation, you must create a
/META-INF/spring.factories file in which you provide the fully qualified name of your
implementation.
|
The following example shows a typical spring.factories file:
org.springframework.cloud.contract.spec.ContractConverter=\
org.springframework.cloud.contract.verifier.converter.YamlContractConverter
Pact Converter
Spring Cloud Contract includes support for Pact representation of contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we present how to add Pact support for your project. Note however that not all functionality is supported. Starting with v3 you can combine multiple matcher for the same element; you can use matchers for the body, headers, request and path; and you can use value generators. Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic. Next to that the request and path matchers are skipped during the conversion. When using a date, time or datetime value generator with a given format, the given format will be skipped and the ISO format will be used.
In order to properly support the Spring Cloud Contract way of doing messaging with Pact you’ll have to provide some additional meta data entries. Below you can find a list of such entries:
-
to define the destination to which a message gets sent, you have to set a
metaDataentry in the Pact file, with keysentToequal to the destination to which a message is to be sent. E.g."metaData": { "sentTo": "activemq:output" }
Pact Contract
Consider following example of a Pact contract, which is a file under the
src/test/resources/contracts folder.
The remainder of this section about using Pact refers to the preceding file.
Pact for Producers
On the producer side, you must add two additional dependencies to your plugin configuration. One is the Spring Cloud Contract Pact support, and the other represents the current Pact version that you use.
When you execute the build of your application, a test will be generated. The generated test might be as follows:
@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/vnd.fraud.v1+json")
.body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
// and:
assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
}
The corresponding generated stub might be as follows:
{
"id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
"request" : {
"url" : "/fraudcheck",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"matches" : "application/vnd\\.fraud\\.v1\\+json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]"
}, {
"matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
"headers" : {
"Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
},
"transformers" : [ "response-template" ]
},
}
Pact for Consumers
On the producer side, you must add two additional dependencies to your project dependencies. One is the Spring Cloud Contract Pact support, and the other represents the current Pact version that you use.
Using the Custom Test Generator
If you want to generate tests for languages other than Java or you are not happy with the way the verifier builds Java tests, you can register your own implementation.
The SingleTestGenerator interface lets you register your own implementation. The
following code listing shows the SingleTestGenerator interface:
Again, you must provide a spring.factories file, such as the one shown in the following
example:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
com.example.MyGenerator
Using the Custom Stub Generator
If you want to generate stubs for stub servers other than WireMock, you can plug in your
own implementation of the StubGenerator interface. The following code listing shows the
StubGenerator interface:
Again, you must provide a spring.factories file, such as the one shown in the following
example:
The default implementation is the WireMock stub generation.
| You can provide multiple stub generator implementations. For example, from a single DSL, you can produce both WireMock stubs and Pact files. |
Using the Custom Stub Runner
If you decide to use a custom stub generation, you also need a custom way of running stubs with your different stub provider.
Assume that you use Moco to build your stubs and that you have written a stub generator and placed your stubs in a JAR file.
In order for Stub Runner to know how to run your stubs, you have to define a custom HTTP Stub server implementation, which might resemble the following example:
Then, you can register it in your spring.factories file, as shown in the following
example:
org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
Now you can run stubs with Moco.
| If you do not provide any implementation, then the default (WireMock) implementation is used. If you provide more than one, the first one on the list is used. |
Using the Custom Stub Downloader
You can customize the way your stubs are downloaded by creating an implementation of the
StubDownloaderBuilder interface, as shown in the following example:
package com.example;
class CustomStubDownloaderBuilder implements StubDownloaderBuilder {
@Override
public StubDownloader build(final StubRunnerOptions stubRunnerOptions) {
return new StubDownloader() {
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration config) {
File unpackedStubs = retrieveStubs();
return new AbstractMap.SimpleEntry<>(
new StubConfiguration(config.getGroupId(), config.getArtifactId(), version,
config.getClassifier()), unpackedStubs);
}
File retrieveStubs() {
// here goes your custom logic to provide a folder where all the stubs reside
}
}
Then you can register it in your spring.factories file, as shown in the following
example:
# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
com.example.CustomStubDownloaderBuilder
Now you can pick a folder with the source of your stubs.
If you do not provide any implementation, then the default is used (scan classpath).
If you provide the stubsMode = StubRunnerProperties.StubsMode.LOCAL or
, stubsMode = StubRunnerProperties.StubsMode.REMOTE then the Aether implementation will be used
If you provide more than one, then the first one on the list is used.
|
Using the SCM Stub Downloader
Whenever the repositoryRoot starts with a SCM protocol
(currently we support only git://), the stub downloader will try
to clone the repository and use it as a source of contracts
to generate tests or stubs.
Either via environment variables, system properties, properties set inside the plugin or contracts repository configuration you can tweak the downloader’s behaviour. Below you can find the list of properties
Type of a property |
Name of the property |
Description |
* * * |
master |
Which branch to checkout |
* * * |
Git clone username |
|
* * * |
Git clone password |
|
* * * |
10 |
Number of attempts to push the commits to |
* * * |
1000 |
Number of millis to wait between attempts to push the commits to |
Using the Pact Stub Downloader
Whenever the repositoryRoot starts with a Pact protocol
(starts with pact://), the stub downloader will try
to fetch the Pact contract definitions from the Pact Broker.
Whatever is set after pact:// will be parsed as the Pact Broker URL.
Either via environment variables, system properties, properties set inside the plugin or contracts repository configuration you can tweak the downloader’s behaviour. Below you can find the list of properties
Name of a property |
Default |
Description |
* * * |
Host from URL passed to |
What is the URL of Pact Broker |
* * * |
Port from URL passed to |
What is the port of Pact Broker |
* * * |
Protocol from URL passed to |
What is the protocol of Pact Broker |
* * * |
Version of the stub, or |
What tags should be used to fetch the stub |
* * * |
|
What kind of authentication should be used to connect to the Pact Broker |
* * * |
The username passed to |
Username used to connect to the Pact Broker |
* * * |
The password passed to |
Password used to connect to the Pact Broker |
* * * |
false |
When |