Converted docs to adoc
This commit is contained in:
@@ -171,4 +171,70 @@ class DslToWireMockClientConverterSpec extends Specification {
|
||||
}
|
||||
''', json, false)
|
||||
}
|
||||
|
||||
def 'should convert dsl to wiremock to show it in the docs'() {
|
||||
given:
|
||||
def converter = new DslToWireMockClientConverter()
|
||||
and:
|
||||
File file = tmpFolder.newFile("dsl_from_docs.groovy")
|
||||
file.write('''
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
priority 1
|
||||
request {
|
||||
method 'POST'
|
||||
url '/users/password'
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body(
|
||||
email: $(stub(optional(regex(email()))), test('abc@abc.com')),
|
||||
callback_url: $(stub(regex(hostname())), test('http://partners.com'))
|
||||
)
|
||||
}
|
||||
response {
|
||||
status 404
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body(
|
||||
code: value(stub("123123"), test(optional("123123"))),
|
||||
message: "User not found by email == [${value(test(regex(email())), stub('not.existing@user.com'))}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
''')
|
||||
when:
|
||||
String json = converter.convertContent("Test", new Contract(file.toPath(), false, 0, null))
|
||||
then:
|
||||
JSONAssert.assertEquals( // tag::wiremock[]
|
||||
'''
|
||||
{
|
||||
"request" : {
|
||||
"url" : "/users/password",
|
||||
"method" : "POST",
|
||||
"bodyPatterns" : [ {
|
||||
"matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,4})?/)]"
|
||||
}, {
|
||||
"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
|
||||
}
|
||||
'''
|
||||
// end::wiremock[]
|
||||
, json, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package io.codearte.accurest.builder
|
||||
|
||||
import io.codearte.accurest.dsl.GroovyDsl
|
||||
import spock.lang.Specification
|
||||
/**
|
||||
* Tests used for the documentation
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class ContractHttpDocsSpec extends Specification {
|
||||
|
||||
GroovyDsl httpDsl =
|
||||
// tag::http_dsl[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.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
|
||||
}
|
||||
// end::http_dsl[]
|
||||
|
||||
GroovyDsl request =
|
||||
// tag::request[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
// HTTP request method (GET/POST/PUT/DELETE).
|
||||
method 'GET'
|
||||
|
||||
// Path component of request URL is specified as follows.
|
||||
urlPath('/users')
|
||||
}
|
||||
|
||||
response {
|
||||
//...
|
||||
}
|
||||
}
|
||||
// end::request[]
|
||||
|
||||
GroovyDsl url =
|
||||
// tag::url[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method 'GET'
|
||||
|
||||
// Specifying `url` and `urlPath` in one contract is illegal.
|
||||
url('http://localhost:8888/users')
|
||||
}
|
||||
|
||||
response {
|
||||
//...
|
||||
}
|
||||
}
|
||||
// end::url[]
|
||||
|
||||
GroovyDsl urlPaths =
|
||||
// tag::urlpath[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.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(stub(containing("[mf]")), server('mf'))
|
||||
|
||||
// `matching` function tests parameter
|
||||
// against passed regular expression.
|
||||
parameter 'offset': value(stub(matching("[0-9]+")), server(123))
|
||||
|
||||
// `notMatching` functions tests if parameter
|
||||
// does not match passed regular expression.
|
||||
parameter 'loginStartsWith': value(stub(notMatching(".{0,2}")), server(3))
|
||||
}
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
|
||||
response {
|
||||
//...
|
||||
}
|
||||
}
|
||||
// end::urlpath[]
|
||||
|
||||
GroovyDsl headers =
|
||||
// tag::headers[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
//...
|
||||
|
||||
// Each header is added in form `'Header-Name' : 'Header-Value'`.
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
|
||||
response {
|
||||
//...
|
||||
}
|
||||
}
|
||||
// end::headers[]
|
||||
|
||||
GroovyDsl body =
|
||||
// tag::body[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
//...
|
||||
|
||||
// JSON and XML formats of request body are supported.
|
||||
// Format will be determined from a header or body's content.
|
||||
body '''{ "login" : "john", "name": "John The Contract" }'''
|
||||
}
|
||||
|
||||
response {
|
||||
//...
|
||||
}
|
||||
}
|
||||
// end::body[]
|
||||
|
||||
GroovyDsl bodyAsXml =
|
||||
// tag::bodyAsXml[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
//...
|
||||
|
||||
// In this case body will be formatted as XML.
|
||||
body equalToXml(
|
||||
'''<user><login>john</login><name>John The Contract</name></user>'''
|
||||
)
|
||||
}
|
||||
|
||||
response {
|
||||
//...
|
||||
}
|
||||
}
|
||||
// end::bodyAsXml[]
|
||||
|
||||
GroovyDsl response =
|
||||
// tag::response[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
//...
|
||||
}
|
||||
response {
|
||||
// Status code sent by the server
|
||||
// in response to request specified above.
|
||||
status 200
|
||||
}
|
||||
}
|
||||
// end::response[]
|
||||
|
||||
GroovyDsl regex =
|
||||
// tag::regex[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method('GET')
|
||||
url $(client(~/\/[0-9]{2}/), server('/12'))
|
||||
}
|
||||
response {
|
||||
status 200
|
||||
body(
|
||||
id: value(
|
||||
client('123'),
|
||||
server(regex('[0-9]+'))
|
||||
),
|
||||
surname: $(
|
||||
client('Kowalsky'),
|
||||
server('Lewandowski')
|
||||
),
|
||||
name: 'Jan',
|
||||
created: $(client('2014-02-02 12:23:43'), server(execute('currentDate(it)'))),
|
||||
correlationId: value(client('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
|
||||
server(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'
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::regex[]
|
||||
|
||||
GroovyDsl optionals =
|
||||
// tag::optionals[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
priority 1
|
||||
request {
|
||||
method 'POST'
|
||||
url '/users/password'
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body(
|
||||
email: $(stub(optional(regex(email()))), test('abc@abc.com')),
|
||||
callback_url: $(stub(regex(hostname())), test('http://partners.com'))
|
||||
)
|
||||
}
|
||||
response {
|
||||
status 404
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body(
|
||||
code: value(stub("123123"), test(optional("123123"))),
|
||||
message: "User not found by email == [${value(test(regex(email())), stub('not.existing@user.com'))}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
// end::optionals[]
|
||||
|
||||
def 'should convert dsl with optionals to proper Spock test'() {
|
||||
given:
|
||||
BlockBuilder blockBuilder = new BlockBuilder(" ")
|
||||
new MockMvcSpockMethodRequestProcessingBodyBuilder(optionals).appendTo(blockBuilder)
|
||||
expect:
|
||||
stripped(blockBuilder.toString()) == stripped(
|
||||
// tag::optionals_test[]
|
||||
"""
|
||||
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("message").matches("User not found by email == \\\\\\\\[[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\\\\\.[a-zA-Z]{2,4}\\\\\\\\]")
|
||||
assertThatJson(parsedJson).field("code").matches("(123123)?")
|
||||
"""
|
||||
// end::optionals_test[]
|
||||
)
|
||||
}
|
||||
|
||||
GroovyDsl method =
|
||||
// tag::method[]
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method 'PUT'
|
||||
url $(client(regex('^/api/[0-9]{2}$')), server('/api/12'))
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body '''\
|
||||
[{
|
||||
"text": "Gonna see you at Warsaw"
|
||||
}]
|
||||
'''
|
||||
}
|
||||
response {
|
||||
body (
|
||||
path: $(client('/api/12'), server(regex('^/api/[0-9]{2}$'))),
|
||||
correlationId: $(client('1223456'), server(execute('isProperCorrelationId($it)')))
|
||||
)
|
||||
status 200
|
||||
}
|
||||
}
|
||||
// end::method[]
|
||||
|
||||
private String stripped(String string) {
|
||||
return string.stripMargin().stripIndent().replace('\t', '').replace('\n', '')
|
||||
}
|
||||
}
|
||||
@@ -591,4 +591,74 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub
|
||||
"JaxRsClientJUnitMethodBodyBuilder" | { GroovyDsl dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl) } | 'method("GET")'
|
||||
}
|
||||
|
||||
def "should generate a call with an url path and query parameters with JUnit - we'll put it into docs"() {
|
||||
given:
|
||||
GroovyDsl contractDsl = GroovyDsl.make {
|
||||
request {
|
||||
method 'GET'
|
||||
urlPath('/users') {
|
||||
queryParameters {
|
||||
parameter 'limit': $(client(equalTo("20")), server(equalTo("10")))
|
||||
parameter 'offset': $(client(containing("20")), server(equalTo("20")))
|
||||
parameter 'filter': "email"
|
||||
parameter 'sort': equalTo("name")
|
||||
parameter 'search': $(client(notMatching(~/^\/[0-9]{2}$/)), server("55"))
|
||||
parameter 'age': $(client(notMatching("^\\w*\$")), server("99"))
|
||||
parameter 'name': $(client(matching("Denis.*")), server("Denis.Stepanov"))
|
||||
parameter 'email': "bob@email.com"
|
||||
parameter 'hello': $(client(matching("Denis.*")), server(absent()))
|
||||
parameter 'hello': absent()
|
||||
}
|
||||
}
|
||||
}
|
||||
response {
|
||||
status 200
|
||||
body """
|
||||
{
|
||||
"property1": "a",
|
||||
"property2": "b"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
MethodBodyBuilder builder = new JaxRsClientJUnitMethodBodyBuilder(contractDsl)
|
||||
BlockBuilder blockBuilder = new BlockBuilder(" ")
|
||||
when:
|
||||
builder.appendTo(blockBuilder)
|
||||
def test = blockBuilder.toString()
|
||||
then:
|
||||
stripped(test) == stripped( // tag::jaxrs[]
|
||||
'''
|
||||
// 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");
|
||||
assertThatJson(parsedJson).field("property2").isEqualTo("b");
|
||||
'''
|
||||
// end::jaxrs[]
|
||||
)
|
||||
and:
|
||||
stubMappingIsValidWireMockStub(contractDsl)
|
||||
}
|
||||
|
||||
private String stripped(String string) {
|
||||
return string.stripMargin().stripIndent().replace('\t', '').replace('\n', '')
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
|
||||
import com.ofg.twitter.place.PairIdController
|
||||
import spock.lang.Specification
|
||||
|
||||
// tag::base_class[]
|
||||
abstract class BaseMockMvcSpec extends Specification {
|
||||
|
||||
def setup() {
|
||||
@@ -19,3 +20,4 @@ abstract class BaseMockMvcSpec extends Specification {
|
||||
}
|
||||
|
||||
}
|
||||
// end::base_class[]
|
||||
|
||||
@@ -1,65 +1,28 @@
|
||||
== Contract DSL
|
||||
|
||||
Contract DSL in Accurest 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, Accurest's DSL is designed to be programmer-readable without any knowledge of the DSL itself.
|
||||
Contract DSL in Accurest 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, Accurest's DSL is designed to be programmer-readable without any knowledge of the DSL itself -
|
||||
it's statically typed.
|
||||
|
||||
Let's look at full example of a contract definition.
|
||||
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method 'POST'
|
||||
urlPath('/users') {
|
||||
queryParameters {
|
||||
parameter 'limit': 100
|
||||
parameter 'offset': containing("1")
|
||||
parameter 'filter': "email"
|
||||
}
|
||||
}
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body '''{ "login" : "john", "name": "John The Contract" }'''
|
||||
}
|
||||
response {
|
||||
status 200
|
||||
headers {
|
||||
header 'Location': '/users/john'
|
||||
}
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/MockMvcMethodBodyBuilderSpec.groovy[tags=dsl_example,indent=0]
|
||||
----
|
||||
|
||||
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 Accurest Contracts to WireMock stubs mapping using standalone maven command: `mvn io.codearte.accurest:accurest-maven-plugin:convert`.
|
||||
|
||||
=== Top-Level Elements
|
||||
=== HTTP Top-Level Elements
|
||||
|
||||
Following methods can be called in the top-level closure of a contract definition. Request and response are mandatory, priority is optional.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.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
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=http_dsl,indent=0]
|
||||
----
|
||||
|
||||
=== Request
|
||||
@@ -68,145 +31,43 @@ HTTP protocol requires only **method and address** to be specified in a request.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
// HTTP request method (GET/POST/PUT/DELETE).
|
||||
method 'GET'
|
||||
|
||||
// Path component of request URL is specified as follows.
|
||||
urlPath('/users')
|
||||
}
|
||||
|
||||
response {
|
||||
...
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=request,indent=0]
|
||||
----
|
||||
|
||||
It is possible to specify whole `url` instead of just path, but `urlPath` is the recommended way as it makes the tests **host-independent**.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method 'GET'
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=url,indent=0]
|
||||
|
||||
// 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`.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.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': containing("[mf]")
|
||||
|
||||
// `matching` function tests parameter
|
||||
// against passed regular expression.
|
||||
parameter 'offset': matching("[0-9]+")
|
||||
|
||||
// `notMatching` functions tests if parameter
|
||||
// does not match passed regular expression.
|
||||
parameter 'loginStartsWith': notMatching(".{0,2}")
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
response {
|
||||
...
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=urlpath,indent=0]
|
||||
----
|
||||
|
||||
It may contain additional **request headers**...
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
...
|
||||
|
||||
// Each header is added in form `'Header-Name' : 'Header-Value'`.
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
response {
|
||||
...
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=headers,indent=0]
|
||||
----
|
||||
|
||||
...and a **request body**.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
...
|
||||
|
||||
// JSON and XML formats of request body are supported.
|
||||
// Format will be determined from a header or body's content.
|
||||
body '''{ "login" : "john", "name": "John The Contract" }'''
|
||||
}
|
||||
|
||||
response {
|
||||
...
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=body,indent=0]
|
||||
----
|
||||
|
||||
**Body's format** can also be specified explicitly by invoking one of format functions.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
...
|
||||
|
||||
// In this case body will be formatted as XML.
|
||||
body equalToXml(
|
||||
'''<user><login>john</login><name>John The Contract</name></user>'''
|
||||
)
|
||||
}
|
||||
|
||||
response {
|
||||
...
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=bodyAsXml,indent=0]
|
||||
----
|
||||
|
||||
=== Response
|
||||
@@ -215,54 +76,21 @@ Minimal response must contain **HTTP status code**.
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
...
|
||||
}
|
||||
response {
|
||||
// Status code sent by the server
|
||||
// in response to request specified above.
|
||||
status 200
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=response,indent=0]
|
||||
----
|
||||
|
||||
Besides status response may contain **headers** and **body**, which are specified the same way as in the request (see previous paragraph).
|
||||
|
||||
=== Regular expressions
|
||||
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.
|
||||
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:
|
||||
Please see the example below:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl groovyDsl == GroovyDsl.make {
|
||||
request {
|
||||
method('GET')
|
||||
url $(client(~/\/[0-9]{2}/), server('/12'))
|
||||
}
|
||||
response {
|
||||
status 200
|
||||
body(
|
||||
id: value(
|
||||
client('123'),
|
||||
server(regex('[0-9]+'))
|
||||
),
|
||||
surname: $(
|
||||
client('Kowalsky'),
|
||||
server('Lewandowski')
|
||||
),
|
||||
name: 'Jan',
|
||||
created: $(client('2014-02-02 12:23:43'), server({ currentDate(it) }))
|
||||
correlationId: value(client('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
|
||||
server(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'
|
||||
}
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=regex,indent=0]
|
||||
----
|
||||
|
||||
=== Passing optional parameters
|
||||
@@ -276,131 +104,41 @@ Example:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
priority 1
|
||||
request {
|
||||
method 'POST'
|
||||
url '/users/password'
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body(
|
||||
email: $(stub(optional(regex(email()))), test('abc@abc.com')),
|
||||
callback_url: $(stub(regex(hostname())), test('http://partners.com'))
|
||||
)
|
||||
}
|
||||
response {
|
||||
status 404
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body(
|
||||
code: value(stub("123123"), test(optional("123123"))),
|
||||
message: "User not found by email == [${value(test(regex(email())), stub('not.existing@user.com'))}]"
|
||||
)
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=optionals,indent=0]
|
||||
----
|
||||
|
||||
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:
|
||||
That way for the example above the following test would be generated if you pick Spock:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
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())
|
||||
!parsedJson.read('''$[?(@.code =~ /(123123)?/)]''', JSONArray).empty
|
||||
!parsedJson.read('''$[?(@.message =~ /User not found by email == \\[[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}\\]/)]''', JSONArray).empty
|
||||
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=optionals_test,indent=0]
|
||||
----
|
||||
|
||||
and the following stub:
|
||||
|
||||
[source,javascript,indent=0]
|
||||
----
|
||||
{
|
||||
"request" : {
|
||||
"url" : "/users/password",
|
||||
"method" : "POST",
|
||||
"bodyPatterns" : [ {
|
||||
"matchesJsonPath" : "$[?(@.callback_url =~ /((http[s]?|ftp):\\/)\\/?([^:\\/\\s]+)(:[0-9]{1,5})?/)]"
|
||||
}, {
|
||||
"matchesJsonPath" : "$[?(@.email =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4})?/)]"
|
||||
} ],
|
||||
"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
|
||||
}
|
||||
include::../../../../accurest-converters/src/test/groovy/io/codearte/accurest/wiremock/DslToWireMockClientConverterSpec.groovy[tags=wiremock,indent=0]
|
||||
----
|
||||
|
||||
=== Executing custom methods on server side
|
||||
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. Please see the examples below:
|
||||
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. Please see the examples below:
|
||||
|
||||
==== Groovy DSL
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method 'PUT'
|
||||
url $(client(regex('^/api/[0-9]{2}$')), server('/api/12'))
|
||||
headers {
|
||||
header 'Content-Type': 'application/json'
|
||||
}
|
||||
body '''\
|
||||
[{
|
||||
"text": "Gonna see you at Warsaw"
|
||||
}]
|
||||
'''
|
||||
}
|
||||
response {
|
||||
body (
|
||||
path: $(client('/api/12'), server(regex('^/api/[0-9]{2}$'))),
|
||||
correlationId: $(client('1223456'), server(execute('isProperCorrelationId($it)')))
|
||||
)
|
||||
status 200
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/ContractHttpDocsSpec.groovy[tags=method,indent=0]
|
||||
----
|
||||
|
||||
==== Base Mock Spec
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
abstract class BaseMockMvcSpec extends Specification {
|
||||
|
||||
def setup() {
|
||||
RestAssuredMockMvc.standaloneSetup(new PairIdController())
|
||||
}
|
||||
|
||||
void isProperCorrelationId(Integer correlationId) {
|
||||
assert correlationId === 123456
|
||||
}
|
||||
}
|
||||
include::../../../../accurest-gradle-plugin/src/test/resources/functionalTest/bootSimple/src/test/groovy/com/ofg/twitter/places/BaseMockMvcSpec.groovy[tags=base_class,indent=0]
|
||||
----
|
||||
|
||||
=== JAX-RS support
|
||||
@@ -419,43 +157,5 @@ Example of a test API generated:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
class FraudDetectionServiceSpec extends MvcSpec {
|
||||
|
||||
def shouldMarkClientAsNotFraud() {
|
||||
when:
|
||||
def response == webTarget
|
||||
.path('/fraudcheck')
|
||||
.request()
|
||||
.method('put', entity('{"clientPesel":"1234567890","loanAmount":123.123}', 'application/vnd.fraud.v1+json'))
|
||||
|
||||
String responseAsString == response.readEntity(String)
|
||||
|
||||
then:
|
||||
response.status === 200
|
||||
response.getHeaderString('Content-Type') === 'application/vnd.fraud.v1+json'
|
||||
and:
|
||||
def responseBody == new JsonSlurper().parseText(responseAsString)
|
||||
responseBody.fraudCheckStatus === "OK"
|
||||
assertThatRejectionReasonIsNull(responseBody.rejectionReason)
|
||||
}
|
||||
|
||||
def shouldMarkClientAsFraud() {
|
||||
when:
|
||||
def response == webTarget
|
||||
.path('/fraudcheck')
|
||||
.request()
|
||||
.method('put', entity('{"clientPesel":"1234567890","loanAmount":99999}', 'application/vnd.fraud.v1+json'))
|
||||
|
||||
String responseAsString == response.readEntity(String)
|
||||
|
||||
then:
|
||||
response.status === 200
|
||||
response.getHeaderString('Content-Type') === 'application/vnd.fraud.v1+json'
|
||||
and:
|
||||
def responseBody == new JsonSlurper().parseText(responseAsString)
|
||||
responseBody.fraudCheckStatus ==~ java.util.regex.Pattern.compile('[A-Z]{5}')
|
||||
responseBody.rejectionReason === "Amount too high"
|
||||
}
|
||||
|
||||
}
|
||||
include::../../../../accurest-core/src/test/groovy/io/codearte/accurest/builder/JaxRsClientMethodBuilderSpec.groovy[tags=jaxrs,indent=0]
|
||||
----
|
||||
BIN
docs/src/docs/asciidoc/images/Deps.png
Normal file
BIN
docs/src/docs/asciidoc/images/Deps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/src/docs/asciidoc/images/Stubs1.png
Normal file
BIN
docs/src/docs/asciidoc/images/Stubs1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/src/docs/asciidoc/images/Stubs2.png
Normal file
BIN
docs/src/docs/asciidoc/images/Stubs2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -2,36 +2,14 @@ Welcome to the Accurest Documentation!
|
||||
|
||||
include::introduction.adoc[]
|
||||
|
||||
include::rest.adoc[]
|
||||
|
||||
include::contract.adoc[]
|
||||
|
||||
include::rest.adoc[]
|
||||
|
||||
include::messaging.adoc[]
|
||||
|
||||
include::stubrunner.adoc[]
|
||||
|
||||
include::stubrunner_msg.adoc[]
|
||||
|
||||
== Migration Guide
|
||||
|
||||
=== Migration to 0.4.7
|
||||
- in 0.4.7 we've fixed package name (coderate to codearte) so you've to do the same in your projects. This means replacing ```io.coderate.accurest.dsl.GroovyDsl``` with ```io.codearte.accurest.dsl.GroovyDsl```
|
||||
|
||||
=== Migration to 1.0.0-RC1
|
||||
- from 1.0.0 we're distinguish ignored contracts from excluded contracts:
|
||||
- `excludedFiles` pattern tells Accurest to skip processing those files at all
|
||||
- `ignoredFiles` pattern tells Accurest to generate contracts and tests, but tests will be marked as `@Ignore`
|
||||
|
||||
- from 1.0.0 the `basePackageForTests` behaviour has changed
|
||||
- prior to the change all DSL files had to be under `contractsDslDir`/`basePackageForTests`/*subpackage* resulting in `basePackageForTests`.*subpackage* test package creation
|
||||
- now all DSL files have to be under `contractsDslDir`/*subpackage* resulting in `basePackageForTests`.*subpackage* test package creation
|
||||
- If you don't migrate to the new approach you will have your tests under `contractsDslDir`.`contractsDslDir`.*subpackage*
|
||||
|
||||
=== Migration to 1.0.7
|
||||
- from 1.0.7 we're setting JUnit as a default testing utility. You have to pass the following option to keep Spock
|
||||
as your first choice:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
include::../../../../accurest-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle[tags=target_framework,indent=0]
|
||||
----
|
||||
include::migration.adoc[]
|
||||
@@ -1,26 +1,80 @@
|
||||
== Introduction
|
||||
|
||||
Just to make long story short - Accurest is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped with __REST Contract Definition Language__ (DSL). Contract definitions are used by Accurest to produce following resources:
|
||||
Just to make long story short - Accurest is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped
|
||||
with __Contract Definition Language__ (DSL). Contract definitions are used by Accurest to produce following resources:
|
||||
|
||||
* JSON stub definitions to be used by Wiremock when doing integration testing on the client code (__client tests__). Test code must still be written by hand, test data is produced by Accurest.
|
||||
* Acceptance tests (in Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__). Full test is generated by Accurest.
|
||||
* JSON stub definitions to be used by Wiremock when doing integration testing on the client code (__client tests__).
|
||||
Test code must still be written by hand, test data is produced by Accurest.
|
||||
* Messaging routes if you're using one. We're integrating with Spring Integration, Spring Cloud Stream and Apache Camel. You can however set your own integrations if you want to
|
||||
* Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (__server tests__). Full test is generated by Accurest.
|
||||
|
||||
Accurest moves TDD to the level of software architecture.
|
||||
|
||||
=== Why?
|
||||
|
||||
The main purposes of Accurest are:
|
||||
Let us assume that we have a system comprising of multiple microservices:
|
||||
|
||||
- to ensure that WireMock stubs (used when developing the client) are doing exactly what actual server-side implementation will do,
|
||||
image::Deps.png[Microservices Architecture]
|
||||
|
||||
==== Testing issues
|
||||
|
||||
If we wanted to test the application in top left corner if it can communicate with other services then we could do one of two things:
|
||||
|
||||
- deploy all microservices and perform end to end tests
|
||||
- mock other microservices in unit / integration tests
|
||||
|
||||
Both have their advantages but also a lot of disadvantages. Let's focus on the latter.
|
||||
|
||||
*Deploy all microservices and perform end to end tests*
|
||||
|
||||
Advantages:
|
||||
- simulates production
|
||||
- tests real communication between services
|
||||
|
||||
Disadvantages:
|
||||
- to test one microservice we would have to deploy 6 microservices, a couple of databases etc.
|
||||
- the environment where the tests would be conducted would be locked for a single suite of tests (i.e. nobody else would be able to run the tests in the meantime).
|
||||
- long to run
|
||||
- very late feedback
|
||||
- extremely hard to debug
|
||||
|
||||
*Mock other microservices in unit / integration tests*
|
||||
|
||||
Advantages:
|
||||
- very fast feedback
|
||||
- no infrastructure requirements
|
||||
|
||||
Disadvantages:
|
||||
- the implementor of the service creates stubs thus they might have nothing to do with the reality
|
||||
- you can go to production with passing tests and failing production
|
||||
|
||||
To solve the aforementioned issues Accurest with Stub Runner were created. Their main idea is to give you very fast feedback, without the need
|
||||
to set up the whole world of microservices.
|
||||
|
||||
image::Stubs1.png[Stubbed Services]
|
||||
|
||||
If you work on stubs then the only applications you need are those that your application is using directly.
|
||||
|
||||
image::Stubs2.png[Stubbed Services]
|
||||
|
||||
Accurest gives you the certainty that the stubs that you're using were created by the service that you're calling. Also if you can use them it means that they were
|
||||
tested against the producer's side. In other words - you can trust those stubs.
|
||||
|
||||
|
||||
=== Purposes
|
||||
|
||||
The main purposes of Accurest with Stub Runner are:
|
||||
|
||||
- to ensure that WireMock / Messaging stubs (used when developing the client) are doing exactly what actual server-side implementation will do,
|
||||
- to promote ATDD method and Microservices architectural style,
|
||||
- to provide a way to publish changes in contracts that are immediately visible on both sides,
|
||||
- to generate boilerplate test code used on the server side.
|
||||
|
||||
=== Client Side
|
||||
|
||||
During the tests you want to have a Wiremock instance up and running that simulates the service Y.
|
||||
During the tests you want to have a Wiremock instance / Messaging route up and running that simulates the service Y.
|
||||
You would like to feed that instance with a proper stub definition. That stub definition would need
|
||||
to be valid from the Wiremock's perspective but should also be reusable on the server side.
|
||||
to be valid and should also be reusable on the server side.
|
||||
|
||||
__Summing it up:__ On this side, in the stub definition, you can use patterns for request stubbing and you need exact
|
||||
values for responses.
|
||||
@@ -37,52 +91,20 @@ that your application behaves in the same way as you define in your stub.
|
||||
__Summing it up:__ On this side, in the stub definition, you need exact values as request and can use patterns/methods
|
||||
for response verification.
|
||||
|
||||
=== Examples
|
||||
=== Dependencies
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
io.codearte.accurest.dsl.GroovyDsl.make {
|
||||
request {
|
||||
method 'PUT'
|
||||
url '/api/12'
|
||||
headers {
|
||||
header 'Content-Type': 'application/vnd.com.ofg.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
|
||||
}
|
||||
}
|
||||
----
|
||||
Accurest and Stub Runner are using the following libraries
|
||||
|
||||
//TODO: Add videos, slides
|
||||
- http://wiremock.org/[WireMock]
|
||||
- https://github.com/jayway/JsonPath[Jayway JSONPath]
|
||||
- https://github.com/marcingrzejszczak/jsonassert[JSONAssert from Marcin Grzejszczak]
|
||||
|
||||
=== Additional readings / videos
|
||||
|
||||
Below you can find some resources related to Accurest and Stub Runner. Note that some can be outdated since the Accurest project
|
||||
is under constant development.
|
||||
|
||||
- https://www.youtube.com/watch?v=daafmTYFoDU[Olga Maciaszek-Sharma talking about Accurest]
|
||||
- https://vimeo.com/130779882[Marcin Grzejszczak and Jakub Kubrynski talking about Accurest]
|
||||
- http://www.slideshare.net/MarcinGrzejszczak/stick-to-the-rules-consumer-driven-contracts-201507-confitura[Slides from Marcin Grzejszczak's talk about Accurest]
|
||||
- http://toomuchcoding.com/blog/categories/accurest/[Accurest article from Marcin Grzejszczak's blog]
|
||||
|
||||
23
docs/src/docs/asciidoc/migration.adoc
Normal file
23
docs/src/docs/asciidoc/migration.adoc
Normal file
@@ -0,0 +1,23 @@
|
||||
== Migration Guide
|
||||
|
||||
=== Migration to 0.4.7
|
||||
- in 0.4.7 we've fixed package name (coderate to codearte) so you've to do the same in your projects. This means replacing ```io.coderate.accurest.dsl.GroovyDsl``` with ```io.codearte.accurest.dsl.GroovyDsl```
|
||||
|
||||
=== Migration to 1.0.0-RC1
|
||||
- from 1.0.0 we're distinguish ignored contracts from excluded contracts:
|
||||
- `excludedFiles` pattern tells Accurest to skip processing those files at all
|
||||
- `ignoredFiles` pattern tells Accurest to generate contracts and tests, but tests will be marked as `@Ignore`
|
||||
|
||||
- from 1.0.0 the `basePackageForTests` behaviour has changed
|
||||
- prior to the change all DSL files had to be under `contractsDslDir`/`basePackageForTests`/*subpackage* resulting in `basePackageForTests`.*subpackage* test package creation
|
||||
- now all DSL files have to be under `contractsDslDir`/*subpackage* resulting in `basePackageForTests`.*subpackage* test package creation
|
||||
- If you don't migrate to the new approach you will have your tests under `contractsDslDir`.`contractsDslDir`.*subpackage*
|
||||
|
||||
=== Migration to 1.0.7
|
||||
- from 1.0.7 we're setting JUnit as a default testing utility. You have to pass the following option to keep Spock
|
||||
as your first choice:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
include::../../../../accurest-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle[tags=target_framework,indent=0]
|
||||
----
|
||||
Reference in New Issue
Block a user