diff --git a/1.1.x/checkstyle-cachefile b/1.1.x/checkstyle-cachefile index f27b272de9..e366483340 100644 --- a/1.1.x/checkstyle-cachefile +++ b/1.1.x/checkstyle-cachefile @@ -1,2 +1,2 @@ -#Tue Aug 29 15:38:29 CEST 2017 +#Wed Aug 30 16:23:44 CEST 2017 configuration*?=EAEA96F3503B3A44B04CCC13A8BC02E9978F3526 diff --git a/1.1.x/multi/multi__contract_dsl.html b/1.1.x/multi/multi__contract_dsl.html new file mode 100644 index 0000000000..6e0cf422b8 --- /dev/null +++ b/1.1.x/multi/multi__contract_dsl.html @@ -0,0 +1,1107 @@ +
+ +![]() | Important |
|---|---|
Remember that inside the contract file you have to provide the fully qualified name to
+the |
Contract DSL is written in Groovy, but don’t be alarmed if you didn’t use Groovy before. Knowledge of the language is not really needed as our DSL uses only +a tiny subset of it (namely literals, method calls and closures). What’s more the DSL is designed to be programmer-readable without any knowledge of the DSL itself - + it’s statically typed.
![]() | 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.
Let’s look at full 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": "http://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
+ }
+ }]
+ '''
+ }
+ response {
+ status 200
+ }
+}Not all features of the DSL are used in example above. If you didn’t find what you are looking for, please check next paragraphs on this page.
You can easily 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 doesn’t support XML properly. Please use JSON or help us implement this feature. |
![]() | Warning |
|---|---|
The support for the verification of size of JSON arrays is experimental. If you want to turn it on please provide
+the value of a system property |
![]() | Warning |
|---|---|
Due to the fact that JSON structure can have any form it’s sometimes impossible to parse it properly when using
+the |
You can add a description to your contract that is nothing else but an arbitrary text. Example:
org.springframework.cloud.contract.spec.Contract.make {
+ description('''
+given:
+ An input
+when:
+ Sth happens
+then:
+ Output
+''')
+ }You can provide a name of your contract. Let’s assume that you’ve provided a name should register a user.
+If you do this then the name of the autogenerated test will be equal to validate_should_register_a_user.
+Also the name of the stub will be should_register_a_user.json in case of a WireMock stub.
![]() | Important |
|---|---|
Please ensure that the name doesn’t contain any characters that will make the generated test + not possible to compile. Also remember that if you provide the same name for multiple contracts then your + autogenerated tests will fail to compile and your generated stubs will override each other. |
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
+}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 whole url instead of just path, but urlPath is the recommended way as it 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 {
+ //...
+ }
+}It may contain additional request headers…
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 {
+ //...
+ }
+}…and a request body.
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. Just call the multipart() method.
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 this example we defined parameters either directly by using the map notation,
+where the value can be a dynamic property (e.g. formParameter: $(consumer(…), producer(…)))
+ or by using the named(…) method that allows you to set a named parameter.
+ A named parameter can set a name and content. You can call it either via
+ a method with 2 arguments: e.g. named("fileName", "fileContent") or
+ via a map notation named(name: "fileName", content: "fileContent").
From this contract the generated test will look more or less like this:
// 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 will look more or less like this:
''' +{ + "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=\\".+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n.+\\r\\n--\\\\1.*" + } ] + }, + "response" : { + "status" : 200, + "transformers" : [ "response-template" ] + } +} + '''
Minimal response must contain HTTP status code.
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 response may contain headers and body, which are specified the same way as in the request (see previous paragraph).
The contract can contain some dynamic properties - timestamps / ids etc. You don’t want to enforce the consumers to stub their
+clocks to always return the same value of time so that it gets matched by the stub. That’s why we allow you to provide the dynamic
+parts in your contracts in two ways. One is to pass them directly in the
+body and one to set them in a separate section called testMatchers and stubMatchers.
You can set the properties inside the body either via the value method
value(consumer(...), producer(...)) +value(c(...), p(...)) +value(stub(...), test(...)) +value(client(...), server(...))
or if you’re using the Groovy map notation for body you can use the $() method
$(consumer(...), producer(...)) +$(c(...), p(...)) +$(stub(...), test(...)) +$(client(...), server(...))
All of the aforementioned approaches are equal. That means that stub and client methods are aliases over the consumer
+method. Let’s take a closer look at what we can do with those values in the subsequent sections.
You can use regular expressions to write your requests in Contract DSL. It is particularly useful when you want to indicate that a given response +should be provided for requests that follow a given pattern. Also, you can use it when you need to use patterns and not exact values both +for your test and your server side tests.
Please see the example below:
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 using a regular expression. If you do that then automatically we’ll +provide the generated string that matches the provided regular expression. For 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 this example for request and response the opposite side of the communication will have the respective data generated.
Spring Cloud Contract comes with a series of predefined regular expressions that you can use in your contracts.
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(/.+/) +protected static final Pattern NON_BLANK = Pattern.compile(/.*(\S+|\R).*|!^\R*$/) +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() +}
so in your contract you can use it like this
Contract dslWithOptionalsInString = Contract.make {
+ priority 1
+ request {
+ method POST()
+ url '/users/password'
+ headers {
+ contentType(applicationJson())
+ }
+ body(
+ email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
+ 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('not.existing@user.com'))}]"
+ )
+ }
+}It is possible to provide optional parameters in your contract. It’s only possible to have optional parameter for the:
Example:
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('abc@abc.com')),
+ 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 are in fact creating a regular expression that should be present 0 or more times.
That way for the example above the following test would be generated if you pick Spock:
""" + given: + def request = given() + .header("Content-Type", "application/json") + .body('''{"email":"abc@abc.com","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)?") +"""
and the following stub:
''' +{ + "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 +} +'''
It is also possible to define a method call to be executed on the server side during the test. Such a method can be added to the class defined as "baseClassForTests" +in the configuration. Example:
Contract
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
+ }
+}Base class
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 can’t use both a String and |
The type of the object read from the JSON can be one of the followings depending on the +JSON path:
String if you point to a String value in a JSONJSONArray if you point to a List in a JSONMap if you point to a Map in a JSONNumber if you point to Integer, Double etc. in a JSONBoolean if you point to a Boolean in a JSONIn the request part of the contract you can specify that the body should be
+taken from a method.
![]() | Important |
|---|---|
You have to provide both the consumer and the producer side
+and the |
Example:
Contract contractDsl = Contract.make {
+ request {
+ method 'GET'
+ url '/something'
+ body(
+ $(c("foo"), p(execute("hashCode()")))
+ )
+ }
+ response {
+ status 200
+ }
+}This will result in calling the hashCode() method in the request body.
+It would more or less like this:
// 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.
+In order to do this you can profit from the fromRequest() method that allows you to reference a bunch
+of elements from the HTTP request. You can use the following options:
fromRequest().url() - return the request URLfromRequest().query(String key) - return the first query parameter with a given namefromRequest().query(String key, int index) - return the nth query parameter with a given namefromRequest().header(String key) - return the first header with a given namefromRequest().header(String key, int index) - return the nth header with a given namefromRequest().body() - return the full request bodyfromRequest().body(String jsonPath) - return the element from the request that matches the JSON PathLet’s take a look at 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 will lead in creation of a test looking more or less like this
// 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("url").isEqualTo("/api/v1/xxxx"); + assertThatJson(parsedJson).field("fullBody").isEqualTo("{\"foo\":\"bar\",\"baz\":5}"); + assertThatJson(parsedJson).field("paramIndex").isEqualTo("bar2"); + assertThatJson(parsedJson).field("responseFoo").isEqualTo("bar"); + assertThatJson(parsedJson).field("authorization2").isEqualTo("secret2"); + assertThatJson(parsedJson).field("responseBaz").isEqualTo(5); + assertThatJson(parsedJson).field("responseBaz2").isEqualTo("Bla bla bar bla bla"); + assertThatJson(parsedJson).field("param").isEqualTo("bar"); + assertThatJson(parsedJson).field("authorization").isEqualTo("secret");
As you can see elements from the request have been properly referenced in the response.
The generated WireMock stub will look more or less like this:
{ + "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" : "{\"url\":\"{{{request.url}}}\",\"param\":\"{{{request.query.foo.[0]}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\",\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\"}", + "headers" : { + "Authorization" : "{{{request.headers.Authorization.[0]}}}" + }, + "transformers" : [ "response-template" ] + } +}
So sending a request as the one presented in the request part of the contract will lead in sending the following
+response body
{ + "url" : "/api/v1/xxxx?foo=bar&foo=bar2", + "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 will work only with WireMock having version greater or equal to 2.5.1. We’re using WireMock’s
+ |
If you’ve been working with Pact this might seem familiar. Quite a few users +are used to having a separation between the body and setting dynamic parts of your contract.
That’s why you can profit from two separate sections. One is called stubMatchers where you can
+define the dynamic values that should end up in a stub. You can set it in the request or inputMessage
+part of your contract. The other is called testMatchers which is present in the response or
+outputMessage side of the contract.
Currently we support only JSON Path based matchers with the following matching possibilities.
+For stubMatchers:
byEquality() - the value taken from the response via the provided JSON Path needs
+to be equal to the provided value in the contractbyRegex(…) - the value taken from the response via the provided JSON Path needs
+to match the regexbyDate() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO DatebyTimestamp() - the value taken from the response via the provided JSON Path needs
+to match the regex for ISO DateTimebyTime() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO TimeFor testMatchers:
byEquality() - the value taken from the response via the provided JSON Path needs
+to be equal to the provided value in the contractbyRegex(…) - the value taken from the response via the provided JSON Path needs
+to match the regexbyDate() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO DatebyTimestamp() - the value taken from the response via the provided JSON Path needs
+to match the regex for ISO DateTimebyTime() - the value taken from the response via the provided JSON Path needs to
+match the regex for ISO TimebyType() - the value taken from the response via the provided JSON Path needs to
+be of the same type as the type defined in the body of the response in the contract.
+byType can take a closure where you can set minOccurrence and maxOccurrence.
+That way you can assert on the size of the flattened collection. To check the size
+of an unflattened collection, use a custom method via byCommand(…) testMatcher.byCommand(…) - the value taken from the response via the provided JSON Path will be
+passed as an input to the custom method that you’re providing. E.g. byCommand('foo($it)')
+will result in calling a foo method to which the value matching the JSON Path will get
+ passed.
The type of the object read from the JSON can be one of the followings depending on the +JSON path:
String if you point to a String value in a JSONJSONArray if you point to a List in a JSONMap if you point to a Map in a JSONNumber if you point to Integer, Double etc. in a JSONBoolean if you point to a Boolean in a JSONLet’s take a look at the following example:
Contract contractDsl = Contract.make {
+ request {
+ method 'GET'
+ urlPath '/get'
+ body([
+ duck: 123,
+ alpha: "abc",
+ number: 123,
+ aBoolean: true,
+ date: "2017-01-01",
+ dateTime: "2017-01-01T01:23:45",
+ time: "01:02:34",
+ valueWithoutAMatcher: "foo",
+ valueWithTypeMatch: "string",
+ 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 this example we’re providing the dynamic portions of the contract in the matchers sections.
+ For the request part you can see that for all fields but valueWithoutAMatcher we’re setting
+ explicitly the values of regular expressions we’d like the stub to contain. For the valueWithoutAMatcher
+ the verification will take place in the same way as without the usage of matchers - the test
+ will perform an equality check in this case.
For the response side in the testMatchers section we’re defining all the dynamic parts
+ in a similar manner. The only difference is that we have the byType matchers too. In that
+ case we’re checking 4 fields in the way that we’re verifying whether the response from the test
+ has a value whose JSON path matching the given field is of the same type as the one defined in the response body and:
$.valueWithTypeMatch - we’re just checking the whether the type is the same$.valueWithMin - we’re checking the type and assert if the size is greater or equal to the min occurrence$.valueWithMax - we’re checking the type and assert if the size is smaller or equal to the max occurrence$.valueWithMinMax - we’re checking the type and assert if the size is between the min and max occurrenceThe resulting test would look more or less like this (note that we’re separating the autogenerated
+assertions and the one from matchers with an and section):
// given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/json") + .body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\"}"); + +// when: + ResponseOptions response = given().spec(request) + .get("/get"); + +// then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/json.*"); +// and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("valueWithoutAMatcher").isEqualTo("foo"); +// and: + assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}"); + assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123); + assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*"); + assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc"); + assertThat(parsedJson.read("$.number", String.class)).matches("-?\\d*(\\.\\d+)?"); + assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)"); + assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"); + assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); + assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); + assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class); + assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class); + assertThat((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 |
and the WireMock stub like this:
''' +{ + "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 |
Let’s look at 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('.+'))
+ }
+ }
+}This will lead in creating the following test (showing just 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. That’s because only the first element of the array got asserted.
+In order to fix this it’s best to apply the assertion to the whole $.events collection and assert it
+via the byCommand(…) method.
We support JAX-RS 2 Client API. Base class needs to define protected WebTarget webTarget and server initialization, right now the only option how to test JAX-RS API is to start a web server.
Request with a body needs to have a content type set otherwise application/octet-stream is going to be used.
In order to use JAX-RS mode, use the following settings:
testMode == 'JAXRSCLIENT'Example of a test API generated:
''' + // 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", "bob@email.com") + .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 etc. then inside your contract you have to provide in the response
+section a async() method. 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 thing that changes in order to fully support context paths is the switch +on the PRODUCER side. The autogenerated tests need to be using the EXPLICIT mode. |
The consumer side remains untouched, in order for the generated test to pass you have to switch the EXPLICIT mode.
Maven. +
<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <testMode>EXPLICIT</testMode> + </configuration> +</plugin>
+
Gradle. +
contracts {
+ testMode = 'EXPLICIT'
+}+
That way you’ll generate a test that DOES NOT use MockMvc. It means that you’re generating +real requests and you need to setup your generated test’s base class to work on a real socket.
Let’s imagine the following contract:
org.springframework.cloud.contract.spec.Contract.make {
+ request {
+ method 'GET'
+ url '/my-context-path/url'
+ }
+ response {
+ status 200
+ }
+}Here is an example of how to set up a base class and Rest Assured for everything to work correctly.
import com.jayway.restassured.RestAssured; +import org.junit.Before; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ContextPathTestingBaseClass { + + @LocalServerPort int port; + + @Before + public void setup() { + RestAssured.baseURI = "http://localhost"; + RestAssured.port = this.port; + } +}
That way all:
/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 output message can be triggered by calling a method (e.g. a Scheduler was started and a message was sent)
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 this case the output message will be sent to output if a method called bookReturnedTriggered will be executed. In the message publisher’s side
+we will generate a test that will call 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.
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 this case the output message will be sent to output if a proper message will be received on the input destination. In the message publisher’s side
+we will generate a test that will send the input message to the defined destination. On the consumer side you can either send a message to the input
+destination or use the some_label to trigger the message.
In HTTP you have a notion of client/stub and `server/test notation. You can use them also in messaging but we’re providing also the consumer and produer methods
+as presented below (note 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'
+ ])
+ }
+}It’s possible to define multiple contracts in one file. An example of such a contract can look like this
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 this example one contract has the name field and the other doesn’t. This will lead to generation of
+two tests that will 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 doesn’t have the name it’s 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 will look like this
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
+contract had index 1 in the list of contracts in the file).
![]() | Tip |
|---|---|
As you can see it’s much better if you name your contracts since then your tests + are far more meaningful. |
It is possible to provide your own functions to the DSL. The key requirement for this +feature was to maintain the static compatibility. Below you will be able to see an example +of:
The full example can be found here.
Below you can find three classes that we will reuse in the DSLs.
PatternUtils contains functions used by both the consumer and the producer.
package com.example; + +import java.util.regex.Pattern; + +/** + * If you want to use {@link Pattern} directly in your tests + * then you can create a class resembling this one. It can + * contain all the {@link Pattern} you want to use in the DSL. + * + * <pre> + * {@code + * request { + * body( + * [ age: $(c(PatternUtils.oldEnough()))] + * ) + * } + * </pre> + * + * Notice that we're using both {@code $()} for dynamic values + * and {@code c()} for the consumer side. + * + * @author Marcin Grzejszczak + */ +//tag::impl[] +public class PatternUtils { + + public static String tooYoung() { + //remove::start[] + return "[0-1][0-9]"; + //remove::end[return] + } + + public static Pattern oldEnough() { + //remove::start[] + return Pattern.compile("[2-9][0-9]"); + //remove::end[return] + } + + /** + * Makes little sense but it's just an example ;) + */ + public static Pattern ok() { + //remove::start[] + return Pattern.compile("OK"); + //remove::end[return] + } +} +//end::impl[]
ConsumerUtils contains functions used by the consumer.
package com.example; + +import org.springframework.cloud.contract.spec.internal.ClientDslProperty; + +/** + * DSL Properties passed to the DSL from the consumer's perspective. + * That means that on the input side {@code Request} for HTTP + * or {@code Input} for messaging you can have a regular expression. + * On the {@code Response} for HTTP or {@code Output} for messaging + * you have to have a concrete value. + * + * @author Marcin Grzejszczak + */ +//tag::impl[] +public class ConsumerUtils { + /** + * Consumer side property. By using the {@link ClientDslProperty} + * you can omit most of boilerplate code from the perspective + * of dynamic values. Example + * + * <pre> + * {@code + * request { + * body( + * [ age: $(ConsumerUtils.oldEnough())] + * ) + * } + * </pre> + * + * That way it's in the implementation that we decide what value we will pass to the consumer + * and which one to the producer. + * + * @author Marcin Grzejszczak + */ + public static ClientDslProperty oldEnough() { + //remove::start[] + // this example is not the best one and + // theoretically you could just pass the regex instead of `ServerDslProperty` but + // it's just to show some new tricks :) + return new ClientDslProperty(PatternUtils.oldEnough(), 40); + //remove::end[return] + } + +} +//end::impl[]
ProducerUtils contains functions used by the producer.
package com.example; + +import org.springframework.cloud.contract.spec.internal.ServerDslProperty; + +/** + * DSL Properties passed to the DSL from the producer's perspective. + * That means that on the input side {@code Request} for HTTP + * or {@code Input} for messaging you have to have a concrete value. + * On the {@code Response} for HTTP or {@code Output} for messaging + * you can have a regular expression. + * + * @author Marcin Grzejszczak + */ +//tag::impl[] +public class ProducerUtils { + + /** + * Producer side property. By using the {@link ProducerUtils} + * you can omit most of boilerplate code from the perspective + * of dynamic values. Example + * + * <pre> + * {@code + * response { + * body( + * [ status: $(ProducerUtils.ok())] + * ) + * } + * </pre> + * + * That way it's in the implementation that we decide what value we will pass to the consumer + * and which one to the producer. + */ + public static ServerDslProperty ok() { + // this example is not the best one and + // theoretically you could just pass the regex instead of `ServerDslProperty` but + // it's just to show some new tricks :) + return new ServerDslProperty( PatternUtils.ok(), "OK"); + } +} +//end::impl[]
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.
First add the common jar dependency as a test dependency. That way since your +contracts files are available at test resources path, automatically the +common jar classes will be visible in your Groovy files.
Maven. +
<dependency> + <groupId>com.example</groupId> + <artifactId>beer-common</artifactId> + <version>${project.version}</version> + <scope>test</scope> +</dependency>
+
Gradle. +
testCompile("com.example:beer-common:0.0.1-SNAPSHOT")+
Now you have to add the dependency for the plugin to reuse at runtime.
Maven. +
<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example</packageWithBaseClasses> + <baseClassMappings> + <baseClassMapping> + <contractPackageRegex>.*intoxication.*</contractPackageRegex> + <baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN> + </baseClassMapping> + </baseClassMappings> + </configuration> + <dependencies> + <dependency> + <groupId>com.example</groupId> + <artifactId>beer-common</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> + </dependencies> +</plugin>
+
Gradle. +
classpath "com.example:beer-common:0.0.1-SNAPSHOT"+
Now you can reference your classes in your DSL. Example:
package contracts.beer.rest + +import com.example.ConsumerUtils +import com.example.ProducerUtils +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description(""" +Represents a successful scenario of getting a beer + +``` +given: + client is old enough +when: + he applies for a beer +then: + we'll grant him the beer +``` + +""") + request { + method 'POST' + url '/check' + body( + age: $(ConsumerUtils.oldEnough()) + ) + headers { + contentType(applicationJson()) + } + } + response { + status 200 + body(""" + { + "status": "${value(ProducerUtils.ok())}" + } + """) + headers { + contentType(applicationJson()) + } + } +}
Here you can find interesting links related to Spring Cloud Contract Verifier:
There are cases where you have your contracts defined in other formats +like YAML, RAML or PACT. On the other hand you’d like to profit from +the test and stubs generation. It’s really easy to add your own implementation +of either of those. Also you can customize the way tests are generated (for example you can generate +tests for other languages) and you can do the same for stubs generation (you can generate +stubs for other stub http server implementations).
Let’s assume that your contract is written in a YAML file like this:
request: + url: /foo + method: PUT + headers: + foo: bar + body: + foo: bar +response: + status: 200 + headers: + foo2: bar + body: + foo2: bar
Thanks to the interface
package org.springframework.cloud.contract.spec + +/** + * Converter to be used to convert FROM {@link File} TO {@link Contract} + * and from {@link Contract} to {@code T} + * + * @param <T> - type to which we want to convert the contract + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +interface ContractConverter<T> { + + /** + * Should this file be accepted by the converter. Can use the file extension + * to check if the conversion is possible. + * + * @param file - file to be considered for conversion + * @return - {@code true} if the given implementation can convert the file + */ + boolean isAccepted(File file) + + /** + * Converts the given {@link File} to its {@link Contract} representation + * + * @param file - file to convert + * @return - {@link Contract} representation of the file + */ + Collection<Contract> convertFrom(File file) + + /** + * Converts the given {@link Contract} to a {@link T} representation + * + * @param contract - the parsed contract + * @return - {@link T} the type to which we do the conversion + */ + T convertTo(Collection<Contract> contract) +}
you can register your own implementation of a contract structure converter. +Your implementation needs to state the condition on which it should start the +conversion. Also you have to define how to perform that conversion in both ways.
![]() | Important |
|---|---|
Once you create your implementation you have to create a |
Example of a spring.factories file
# Converters +org.springframework.cloud.contract.spec.ContractConverter=\ +org.springframework.cloud.contract.verifier.converter.YamlContractConverter
and the YAML implementation
package org.springframework.cloud.contract.verifier.converter + +import java.nio.file.Files + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractConverter +import org.springframework.cloud.contract.spec.internal.Headers +import org.yaml.snakeyaml.Yaml + +/** + * Simple converter from and to a {@link YamlContract} to a collection of {@link Contract} + */ +@CompileStatic +class YamlContractConverter implements ContractConverter<List<YamlContract>> { + + @Override + public boolean isAccepted(File file) { + String name = file.getName() + return name.endsWith(".yml") || name.endsWith(".yaml") + } + + @Override + public Collection<Contract> convertFrom(File file) { + try { + YamlContract yamlContract = new Yaml().loadAs( + Files.newInputStream(file.toPath()), YamlContract.class) + return [Contract.make { + request { + method(yamlContract?.request?.method) + url(yamlContract?.request?.url) + headers { + yamlContract?.request?.headers?.each { String key, Object value -> + header(key, value) + } + } + body(yamlContract?.request?.body) + } + response { + status(yamlContract?.response?.status) + headers { + yamlContract?.response?.headers?.each { String key, Object value -> + header(key, value) + } + } + body(yamlContract?.response?.body) + } + }] + } + catch (FileNotFoundException e) { + throw new IllegalStateException(e) + } + } + + @Override + public List<YamlContract> convertTo(Collection<Contract> contracts) { + return contracts.collect { Contract contract -> + YamlContract yamlContract = new YamlContract() + yamlContract.request.with { + method = contract?.request?.method?.clientValue + url = contract?.request?.url?.clientValue + headers = (contract?.request?.headers as Headers)?.asStubSideMap() + body = contract?.request?.body?.clientValue as Map + } + yamlContract.response.with { + status = contract?.response?.status?.clientValue as Integer + headers = (contract?.response?.headers as Headers)?.asStubSideMap() + body = contract?.response?.body?.clientValue as Map + } + return yamlContract + } + } +}
Spring Cloud Contract comes with an out of the box support for Pact representation of contracts. +In other words instead of using the Groovy DSL you can use Pact files. In this section +we will present how to add such a support for your project.
We will be working on the following example of a Pact contract. We’ve placed this file under
+the src/test/resources/contracts folder.
{
+ "provider": {
+ "name": "Provider"
+ },
+ "consumer": {
+ "name": "Consumer"
+ },
+ "interactions": [
+ {
+ "description": "",
+ "request": {
+ "method": "PUT",
+ "path": "/fraudcheck",
+ "headers": {
+ "Content-Type": "application/vnd.fraud.v1+json"
+ },
+ "body": {
+ "clientId": "1234567890",
+ "loanAmount": 99999
+ },
+ "matchingRules": {
+ "$.body.clientId": {
+ "match": "regex",
+ "regex": "[0-9]{10}"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
+ },
+ "body": {
+ "fraudCheckStatus": "FRAUD",
+ "rejectionReason": "Amount too high"
+ },
+ "matchingRules": {
+ "$.body.fraudCheckStatus": {
+ "match": "regex",
+ "regex": "FRAUD"
+ }
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "pact-specification": {
+ "version": "2.0.0"
+ },
+ "pact-jvm": {
+ "version": "2.4.18"
+ }
+ }
+}On the producer side you have add to your plugin configuration two additional dependencies. +One is the Spring Cloud Contract Pact support and the other represents the current +Pact version that you’re using.
Maven. +
<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses> + </configuration> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-spec-pact</artifactId> + <version>${spring-cloud-contract.version}</version> + </dependency> + <dependency> + <groupId>au.com.dius</groupId> + <artifactId>pact-jvm-model</artifactId> + <version>2.4.18</version> + </dependency> + </dependencies> +</plugin>
+
Gradle. +
classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}" +classpath 'au.com.dius:pact-jvm-model:2.4.18'
+
When you execute the build of your application a test, looking more or less like this, will be generated
@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")).isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8"); + // 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"); +}
and the stub looking like this
{
+ "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
+ "request" : {
+ "url" : "/fraudcheck",
+ "method" : "PUT",
+ "headers" : {
+ "Content-Type" : {
+ "equalTo" : "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"
+ }
+ }
+}On the producer side you have add to your project dependencies two additional dependencies. +One is the Spring Cloud Contract Pact support and the other represents the current +Pact version that you’re using.
Maven. +
<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-spec-pact</artifactId> + <scope>test</scope> +</dependency> +<dependency> + <groupId>au.com.dius</groupId> + <artifactId>pact-jvm-model</artifactId> + <version>2.4.18</version> + <scope>test</scope> +</dependency>
+
Gradle. +
testCompile "org.springframework.cloud:spring-cloud-contract-spec-pact" +testCompile 'au.com.dius:pact-jvm-model:2.4.18'
+
If you want to generate tests for different languages than Java or you’re +not happy with the way we’re building Java tests for you then you can register +your own implementation to do that.
Thanks to the interface
package org.springframework.cloud.contract.verifier.builder + +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.file.ContractMetadata +/** + * Builds a single test. + * + * @since 1.1.0 + */ +interface SingleTestGenerator { + + /** + * Creates contents of a single test class in which all test scenarios from + * the contract metadata should be placed. + * + * @param properties - properties passed to the plugin + * @param listOfFiles - list of parsed contracts with additional metadata + * @param className - the name of the generated test class + * @param classPackage - the name of the package in which the test class should be stored + * @param includedDirectoryRelativePath - relative path to the included directory + * @return contents of a single test class + */ + String buildClass(ContractVerifierConfigProperties properties, Collection<ContractMetadata> listOfFiles, + String className, String classPackage, String includedDirectoryRelativePath) + + /** + * Extension that should be appended to the generated test class. E.g. {@code .java} or {@code .php} + * + * @param properties - properties passed to the plugin + */ + String fileExtension(ContractVerifierConfigProperties properties) +}
you can register your own implementation that generates a test. Again, it’s enough to provide
+a proper spring.factories file. Example:
org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/ +com.example.MyGenerator
If you want to generate stubs for other stub server than WireMock it’s enough to + plug in your own implementation of this interface:
package org.springframework.cloud.contract.verifier.converter + +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * Converts contracts into their stub representation. + * + * @since 1.1.0 + */ +@CompileStatic +interface StubGenerator { + + /** + * Returns {@code true} if the converter can handle the file to convert it into a stub. + */ + boolean canHandleFileName(String fileName) + + /** + * Returns the collection of converted contracts into stubs. One contract can + * result in multiple stubs. + */ + Map<Contract, String> convertContents(String rootName, ContractMetadata content) + + /** + * Returns the name of the converted stub file. If you have multiple contracts + * in a single file then a prefix will be added to the generated file. If you + * provide the {@link Contract#name} field then that field will override the + * generated file name. + * + * Example: name of file with 2 contracts is {@code foo.groovy}, it will be + * converted by the implementation to {@code foo.json}. The recursive file + * converter will create two files {@code 0_foo.json} and {@code 1_foo.json} + */ + String generateOutputFileNameForInput(String inputFileName) +}
you can register your own implementation that generate Stubs. Again, it’s enough to provide
+a proper spring.factories file. Example:
# Stub converters +org.springframework.cloud.contract.verifier.converter.StubGenerator=\ +org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter
The default implementation is the WireMock stub generation.
![]() | Tip |
|---|---|
You can provide multiple stub generator implementations. That way for example from a single +DSL as input you can e.g. produce WireMock stubs and Pact files too! |
If you decide to have a custom stub generation you also need a custom way of running +stubs with your different stub provider.
Let us assume that you’re using Moco to build your stubs. +You wrote a proper stub generator and your stubs got placed 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. It can look like this:
package org.springframework.cloud.contract.stubrunner.provider.moco + +import com.github.dreamhead.moco.bootstrap.arg.HttpArgs +import com.github.dreamhead.moco.runner.JsonRunner +import com.github.dreamhead.moco.runner.RunnerSetting +import groovy.util.logging.Slf4j +import org.springframework.cloud.contract.stubrunner.HttpServerStub +import org.springframework.util.SocketUtils + +@Slf4j +class MocoHttpServerStub implements HttpServerStub { + + private boolean started + private JsonRunner runner + private int port + + @Override + int port() { + if (!isRunning()) { + return -1 + } + return port + } + + @Override + boolean isRunning() { + return started + } + + @Override + HttpServerStub start() { + return start(SocketUtils.findAvailableTcpPort()) + } + + @Override + HttpServerStub start(int port) { + this.port = port + return this + } + + @Override + HttpServerStub stop() { + if (!isRunning()) { + return this + } + this.runner.stop() + return this + } + + @Override + HttpServerStub registerMappings(Collection<File> stubFiles) { + List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") } + .collect { + log.info("Trying to parse [{}]", it.name) + try { + return RunnerSetting.aRunnerSetting().withStream(it.newInputStream()).build() + } catch (Exception e) { + log.warn("Exception occurred while trying to parse file [{}]", it.name, e) + return null + } + }.findAll { it } + this.runner = JsonRunner.newJsonRunnerWithSetting(settings, + HttpArgs.httpArgs().withPort(this.port).build()) + this.runner.run() + this.started = true + return this + } + + @Override + boolean isAccepted(File file) { + return file.name.endsWith(".json") + } +}
and just register it in your spring.factories file
org.springframework.cloud.contract.stubrunner.HttpServerStub=\ +org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
that way you’ll be able to run stubs using Moco.
![]() | Important |
|---|---|
If you don’t provide any implementation then the default one - WireMock based +will be picked. If you provide more than one then the first one on the list will be picked. |
You can customize the way your stubs are downloaded. It’s enough to create an
+implementation of the StubDownloaderBuilder
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 + } +}
and just register it in your spring.factories file
# Example of a custom Stub Downloader Provider +org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\ +com.example.CustomStubDownloaderBuilder
that way you’ll be able to pick a folder with the source of your stubs.
![]() | Important |
|---|---|
If you don’t provide any implementation then the default one will be picked.
+ If you provide |
What you always need is confidence in pushing new features into a new application or service in a distributed system. +
What you always need is confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts and service schemas in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers -and consumers, for HTTP and message-based interactions.