This version is still in development and is not considered stable yet. For the latest stable version, please use spring-cloud-contract 4.2.0! |
HTTP Contracts
This page describes most important HTTP related parts of the contract.
HTTP Top-Level Elements
You can call the following methods in the top-level closure of a contract definition:
-
request
: Mandatory -
response
: Mandatory -
priority
: Optional
The following example shows how to define an HTTP request contract:
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 {
method GET()
url "/foo"
//...
}
// 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 {
status 200
//...
}
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
priority 1
}
priority: 8
request:
...
response:
...
org.springframework.cloud.contract.spec.Contract.make(c -> {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
c.request(r -> {
r.method(r.GET());
r.url("/foo");
// ...
});
// 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).
c.response(r -> {
r.status(200);
// ...
});
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
c.priority(1);
});
contract {
// Definition of HTTP request part of the contract
// (this can be a valid request or invalid depending
// on type of contract being specified).
request {
method = GET
url = url("/foo")
// ...
}
// 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 {
status = OK
// ...
}
// Contract priority, which can be used for overriding
// contracts (1 is highest). Priority is optional.
priority = 1
}
If you want to make your contract have a higher priority,
you need to pass a lower number to the priority tag or method. For example, a priority with
a value of 5 has higher priority than a priority with a value of 10 .
|
HTTP Request
The HTTP protocol requires only the method and the URL to be specified in a request. The same information is mandatory in request definition of the contract.
The following example shows a contract for a request:
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 {
//...
status 200
}
}
method: PUT
url: /foo
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// HTTP request method (GET/POST/PUT/DELETE).
r.method("GET");
// Path component of request URL is specified as follows.
r.urlPath("/users");
});
c.response(r -> {
// ...
r.status(200);
});
});
contract {
request {
// HTTP request method (GET/POST/PUT/DELETE).
method = method("GET")
// Path component of request URL is specified as follows.
urlPath = path("/users")
}
response {
// ...
status = code(200)
}
}
You can specify an absolute rather than a relative url
, but using urlPath
is
the recommended way, as doing so makes the tests be host-independent.
The following example uses url
:
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 {
//...
status 200
}
}
request:
method: PUT
urlPath: /foo
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("GET");
// Specifying `url` and `urlPath` in one contract is illegal.
r.url("http://localhost:8888/users");
});
c.response(r -> {
// ...
r.status(200);
});
});
contract {
request {
method = GET
// Specifying `url` and `urlPath` in one contract is illegal.
url("http://localhost:8888/users")
}
response {
// ...
status = OK
}
}
request
may contain query parameters, as the following example (which uses urlPath
) shows:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
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 {
//...
status 200
}
}
request:
...
queryParameters:
a: b
b: c
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.urlPath("/users", u -> {
// 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.
u.queryParameters(q -> {
// If a simple literal is used as value
// default matcher function is used (equalTo)
q.parameter("limit", 100);
// `equalTo` function simply compares passed value
// using identity operator (==).
q.parameter("filter", r.equalTo("email"));
// `containing` function matches strings
// that contains passed substring.
q.parameter("gender", r.value(r.consumer(r.containing("[mf]")), r.producer("mf")));
// `matching` function tests parameter
// against passed regular expression.
q.parameter("offset", r.value(r.consumer(r.matching("[0-9]+")), r.producer(123)));
// `notMatching` functions tests if parameter
// does not match passed regular expression.
q.parameter("loginStartsWith", r.value(r.consumer(r.notMatching(".{0,2}")), r.producer(3)));
});
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
contract {
request {
// ...
method = GET
// 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.
urlPath = path("/users") withQueryParameters {
// 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 {
// ...
status = code(200)
}
}
If a query parameter is missing in the contract it doesn’t mean that we expect a request to be matched if the query parameter is missing. Quite the contrary, that means that the query parameter is not necessary to be there for the request to be matched. |
request
can contain additional request headers, as the following example shows:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
headers {
header 'key': 'value'
contentType(applicationJson())
}
//...
}
response {
//...
status 200
}
}
request:
...
headers:
foo: bar
fooReq: baz
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper methods
r.headers(h -> {
h.header("key", "value");
h.contentType(h.applicationJson());
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
contract {
request {
// ...
method = GET
url = url("/foo")
// Each header is added in form `'Header-Name' : 'Header-Value'`.
// there are also some helper variables
headers {
header("key", "value")
contentType = APPLICATION_JSON
}
// ...
}
response {
// ...
status = OK
}
}
request
may contain additional request cookies, as the following example shows:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"
// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
// there are also some helper methods
cookies {
cookie 'key': 'value'
cookie('another_key', 'another_value')
}
//...
}
response {
//...
status 200
}
}
request:
...
cookies:
foo: bar
fooReq: baz
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
// there are also some helper methods
r.cookies(ck -> {
ck.cookie("key", "value");
ck.cookie("another_key", "another_value");
});
// ...
});
c.response(r -> {
// ...
r.status(200);
});
});
contract {
request {
// ...
method = GET
url = url("/foo")
// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
// there are also some helper methods
cookies {
cookie("key", "value")
cookie("another_key", "another_value")
}
// ...
}
response {
// ...
status = code(200)
}
}
request
may contain a request body, as the following example shows:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"
// 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 {
//...
status 200
}
}
request:
...
body:
foo: bar
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
// Currently only JSON format of request body is supported.
// Format will be determined from a header or body's content.
r.body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }");
});
c.response(r -> {
// ...
r.status(200);
});
});
contract {
request {
// ...
method = GET
url = url("/foo")
// Currently only JSON format of request body is supported.
// Format will be determined from a header or body's content.
body = body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }")
}
response {
// ...
status = OK
}
}
request
can contain multipart elements. To include multipart elements, use the
multipart
method/section, as the following examples show:
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')),
// content type for the part
contentType: $(c(regex(nonEmpty())), p('application/json')))
)
}
response {
status OK()
}
}
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
file: named(
name: value(stub(regex('.+')), test('file')),
content: value(stub(regex('.+')), test([100, 117, 100, 97] as byte[]))
)
)
}
response {
status 200
}
}
request:
method: PUT
url: /multipart
headers:
Content-Type: multipart/form-data;boundary=AaB03x
multipart:
params:
# key (parameter name), value (parameter value) pair
formParameter: '"formParameterValue"'
someBooleanParameter: true
named:
- paramName: file
fileName: filename.csv
fileContent: file content
matchers:
multipart:
params:
- key: formParameter
regex: ".+"
- key: someBooleanParameter
predefined: any_boolean
named:
- paramName: file
fileName:
predefined: non_empty
fileContent:
predefined: non_empty
response:
status: 200
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_multipart implements Supplier<Collection<Contract>> {
private static Map<String, DslProperty> namedProps(Request r) {
Map<String, DslProperty> map = new HashMap<>();
// name of the file
map.put("name", r.$(r.c(r.regex(r.nonEmpty())), r.p("filename.csv")));
// content of the file
map.put("content", r.$(r.c(r.regex(r.nonEmpty())), r.p("file content")));
// content type for the part
map.put("contentType", r.$(r.c(r.regex(r.nonEmpty())), r.p("application/json")));
return map;
}
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
r.method("PUT");
r.url("/multipart");
r.headers(h -> {
h.contentType("multipart/form-data;boundary=AaB03x");
});
r.multipart(ContractVerifierUtil.map()
// key (parameter name), value (parameter value) pair
.entry("formParameter", r.$(r.c(r.regex("\".+\"")), r.p("\"formParameterValue\"")))
.entry("someBooleanParameter", r.$(r.c(r.regex(r.anyBoolean())), r.p("true")))
// a named parameter (e.g. with `file` name) that represents file
// with
// `name` and `content`. You can also call `named("fileName",
// "fileContent")`
.entry("file", r.named(namedProps(r))));
});
c.response(r -> {
r.status(r.OK());
});
}));
}
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
contract {
request {
method = PUT
url = url("/multipart")
multipart {
field("formParameter", value(consumer(regex("\".+\"")), producer("\"formParameterValue\"")))
field("someBooleanParameter", value(consumer(anyBoolean), producer("true")))
field("file",
named(
// name of the file
value(consumer(regex(nonEmpty)), producer("filename.csv")),
// content of the file
value(consumer(regex(nonEmpty)), producer("file content")),
// content type for the part
value(consumer(regex(nonEmpty)), producer("application/json"))
)
)
}
headers {
contentType = "multipart/form-data;boundary=AaB03x"
}
}
response {
status = OK
}
}
In the preceding example, we defined 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 aname
andcontent
. You can call it either by using a method with two arguments, such asnamed("fileName", "fileContent")
, or by using a map notation, such asnamed(name: "fileName", content: "fileContent")
.
-
The multipart parameters are set in the
multipart.params
section. -
The named parameters (the
fileName
andfileContent
for a given parameter name) can be set in themultipart.named
section. That section contains theparamName
(the name of the parameter),fileName
(the name of the file),fileContent
(the content of the file) fields. -
The dynamic bits can be set in the
matchers.multipart
section.-
For parameters, use the
params
section, which can acceptregex
or apredefined
regular expression. -
For named parameters, use the
named
section where you first define the parameter name withparamName
. Then you can pass the parametrization of eitherfileName
orfileContent
in aregex
or in apredefined
regular expression.
-
For the named(…) section you always have to add a pair of
value(producer(…), consumer(…)) calls. Just setting DSL properties such
as just value(producer(…)) or just file(…) will not work.
Check this issue for more information.
|
From the contract in the preceding example, the generated test and stub look 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);
'''
{
"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-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n\\".+\\"\\r?\\n--.*"
}, {
"matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n(true|false)\\r?\\n--.*"
}, {
"matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n[\\\\S\\\\s]+\\r?\\n--.*"
} ]
},
"response" : {
"status" : 200,
"transformers" : [ "response-template", "foo-transformer" ]
}
}
'''
HTTP Response
The response must contain an HTTP status code and may contain other information. The following code shows an example:
org.springframework.cloud.contract.spec.Contract.make {
request {
//...
method GET()
url "/foo"
}
response {
// Status code sent by the server
// in response to request specified above.
status OK()
}
}
response:
...
status: 200
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
// ...
r.method(r.GET());
r.url("/foo");
});
c.response(r -> {
// Status code sent by the server
// in response to request specified above.
r.status(r.OK());
});
});
contract {
request {
// ...
method = GET
url =url("/foo")
}
response {
// Status code sent by the server
// in response to request specified above.
status = OK
}
}
Besides status, the response may contain headers, cookies, and a body, which are specified the same way as in the request (see HTTP Request).
In the Groovy DSL, you can reference the org.springframework.cloud.contract.spec.internal.HttpStatus
methods to provide a meaningful status instead of a digit. For example, you can call
OK() for a status 200 or BAD_REQUEST() for 400 .
|
XML Support for HTTP
For HTTP contracts, we also support using XML in the 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 the request and the 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 the second argument. All the body matchers apart from byType()
are supported.
The following example shows a Groovy DSL contract with XML in the response body:
Contract.make {
request {
method GET()
urlPath '/get'
headers {
contentType(applicationXml())
}
}
response {
status(OK())
headers {
contentType(applicationXml())
}
body """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<key><complex>foo</complex></key>
</test>"""
bodyMatchers {
xPath('/test/duck/text()', byRegex("[0-9]{3}"))
xPath('/test/duck/text()', byCommand('equals($it)'))
xPath('/test/duck/xxx', byNull())
xPath('/test/duck/text()', byEquality())
xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
xPath('/test/alpha/text()', byEquality())
xPath('/test/number/text()', byRegex(number()))
xPath('/test/date/text()', byDate())
xPath('/test/dateTime/text()', byTimestamp())
xPath('/test/time/text()', byTime())
xPath('/test/*/complex/text()', byEquality())
xPath('/test/duck/@type', byEquality())
}
}
}
Contract.make {
request {
method GET()
urlPath '/get'
headers {
contentType(applicationXml())
}
}
response {
status(OK())
headers {
contentType(applicationXml())
}
body """
<ns1:test xmlns:ns1="http://demo.com/testns">
<ns1:header>
<duck-bucket type='bigbucket'>
<duck>duck5150</duck>
</duck-bucket>
</ns1:header>
</ns1:test>
"""
bodyMatchers {
xPath('/test/duck/text()', byRegex("[0-9]{3}"))
xPath('/test/duck/text()', byCommand('equals($it)'))
xPath('/test/duck/xxx', byNull())
xPath('/test/duck/text()', byEquality())
xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
xPath('/test/alpha/text()', byEquality())
xPath('/test/number/text()', byRegex(number()))
xPath('/test/date/text()', byDate())
xPath('/test/dateTime/text()', byTimestamp())
xPath('/test/time/text()', byTime())
xPath('/test/duck/@type', byEquality())
}
}
}
Contract.make {
request {
method GET()
urlPath '/get'
headers {
contentType(applicationXml())
}
}
response {
status(OK())
headers {
contentType(applicationXml())
}
body """
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<RsHeader xmlns="http://schemas.xmlsoap.org/soap/custom">
<MsgSeqId>1234</MsgSeqId>
</RsHeader>
</SOAP-ENV:Header>
</SOAP-ENV:Envelope>
"""
bodyMatchers {
xPath('//*[local-name()=\'RsHeader\' and namespace-uri()=\'http://schemas.xmlsoap.org/soap/custom\']/*[local-name()=\'MsgSeqId\']/text()', byEquality())
}
}
}
Contract.make {
request {
method GET()
urlPath '/get'
headers {
contentType(applicationXml())
}
}
response {
status(OK())
headers {
contentType(applicationXml())
}
body """
<ns1:customer xmlns:ns1="http://demo.com/customer" xmlns:addr="http://demo.com/address">
<email>[email protected]</email>
<contact-info xmlns="http://demo.com/contact-info">
<name>Krombopulous</name>
<address>
<addr:gps>
<lat>51</lat>
<addr:lon>50</addr:lon>
</addr:gps>
</address>
</contact-info>
</ns1:customer>
"""
}
}
request:
method: GET
url: /getymlResponse
headers:
Content-Type: application/xml
body: |
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
matchers:
body:
- path: /test/duck/text()
type: by_regex
value: "[0-9]{10}"
- path: /test/duck/text()
type: by_equality
- path: /test/time/text()
type: by_time
response:
status: 200
headers:
Content-Type: application/xml
body: |
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<valueWithTypeMatch>string</valueWithTypeMatch>
<key><complex>foo</complex></key>
</test>
matchers:
body:
- path: /test/duck/text()
type: by_regex
value: "[0-9]{10}"
- path: /test/duck/text()
type: by_command
value: "test($it)"
- path: /test/duck/xxx
type: by_null
- path: /test/duck/text()
type: by_equality
- path: /test/time/text()
type: by_time
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
class contract_xml implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method(r.GET());
r.urlPath("/get");
r.headers(h -> {
h.contentType(h.applicationXml());
});
});
c.response(r -> {
r.status(r.OK());
r.headers(h -> {
h.contentType(h.applicationXml());
});
r.body("<test>\n" + "<duck type='xtype'>123</duck>\n" + "<alpha>abc</alpha>\n" + "<list>\n"
+ "<elem>abc</elem>\n" + "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
+ "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n" + "<date>2017-01-01</date>\n"
+ "<dateTime>2017-01-01T01:23:45</dateTime>\n" + "<time>01:02:34</time>\n"
+ "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n" + "<key><complex>foo</complex></key>\n"
+ "</test>");
r.bodyMatchers(m -> {
m.xPath("/test/duck/text()", m.byRegex("[0-9]{3}"));
m.xPath("/test/duck/text()", m.byCommand("equals($it)"));
m.xPath("/test/duck/xxx", m.byNull());
m.xPath("/test/duck/text()", m.byEquality());
m.xPath("/test/alpha/text()", m.byRegex(r.onlyAlphaUnicode()));
m.xPath("/test/alpha/text()", m.byEquality());
m.xPath("/test/number/text()", m.byRegex(r.number()));
m.xPath("/test/date/text()", m.byDate());
m.xPath("/test/dateTime/text()", m.byTimestamp());
m.xPath("/test/time/text()", m.byTime());
m.xPath("/test/*/complex/text()", m.byEquality());
m.xPath("/test/duck/@type", m.byEquality());
});
});
});
};
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
contract {
request {
method = GET
urlPath = path("/get")
headers {
contentType = APPLICATION_XML
}
}
response {
status = OK
headers {
contentType =APPLICATION_XML
}
body = body("<test>\n" + "<duck type='xtype'>123</duck>\n"
+ "<alpha>abc</alpha>\n" + "<list>\n" + "<elem>abc</elem>\n"
+ "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
+ "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n"
+ "<date>2017-01-01</date>\n"
+ "<dateTime>2017-01-01T01:23:45</dateTime>\n"
+ "<time>01:02:34</time>\n"
+ "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n"
+ "<key><complex>foo</complex></key>\n" + "</test>")
bodyMatchers {
xPath("/test/duck/text()", byRegex("[0-9]{3}"))
xPath("/test/duck/text()", byCommand("equals(\$it)"))
xPath("/test/duck/xxx", byNull)
xPath("/test/duck/text()", byEquality)
xPath("/test/alpha/text()", byRegex(onlyAlphaUnicode))
xPath("/test/alpha/text()", byEquality)
xPath("/test/number/text()", byRegex(number))
xPath("/test/date/text()", byDate)
xPath("/test/dateTime/text()", byTimestamp)
xPath("/test/time/text()", byTime)
xPath("/test/*/complex/text()", byEquality)
xPath("/test/duck/@type", byEquality)
}
}
}
The following example shows an automatically generated test for XML in the 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");
}
XML Support for Namespaces
Namespaced XML is supported. However, any XPath expresssions used to select namespaced content must be updated.
Consider the following explicitly namespaced XML document:
<ns1:customer xmlns:ns1="http://demo.com/customer">
<email>[email protected]</email>
</ns1:customer>
The XPath expression to select the email address is: /ns1:customer/email/text()
.
Beware as the unqualified expression (/customer/email/text() ) results in "" .
|
For content that uses an unqualified namespace, the expression is more verbose. Consider the following XML document that uses an unqualified namespace:
<customer xmlns="http://demo.com/customer">
<email>[email protected]</email>
</customer>
The XPath expression to select the email address is
*/[local-name()='customer' and namespace-uri()='http://demo.com/customer']/*[local-name()='email']/text()
Beware, as the unqualified expressions (/customer/email/text() or */[local-name()='customer' and namespace-uri()='http://demo.com/customer']/email/text() )
result in "" . Even the child elements have to be referenced with the local-name syntax.
|
General Namespaced Node Expression Syntax
-
Node using qualified namespace:
/<node-name>
-
Node using and defining an unqualified namespace:
/*[local-name=()='<node-name>' and namespace-uri=()='<namespace-uri>']
In some cases, you can omit the namespace_uri portion, but doing so may lead to ambiguity.
|
-
Node using an unqualified namespace (one of its ancestor’s defines the xmlns attribute):
/*[local-name=()='<node-name>']
Asynchronous Support
If you use 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
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
// ...
});
c.response(r -> {
r.async();
// ...
});
}));
}
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
contract {
request {
// ...
}
response {
async = true
// ...
}
}
You can also use the fixedDelayMilliseconds
method or property to add delay to your stubs.
The following example shows how to do so:
org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url '/get'
}
response {
status 200
body 'Passed'
fixedDelayMilliseconds 1000
}
}
response:
fixedDelayMilliseconds: 1000
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.request(r -> {
// ...
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
// ...
});
}));
}
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
contract {
request {
// ...
}
response {
delay = fixedMilliseconds(1000)
// ...
}
}