Merge branch '1.0.x'

This commit is contained in:
Marcin Grzejszczak
2017-02-23 17:37:40 +01:00
11 changed files with 550 additions and 3 deletions

View File

@@ -15,6 +15,8 @@ and consumers, for HTTP and message-based interactions.
:core_path: ../../../..
:doc_samples: {core_path}/samples/wiremock-jetty
:wiremock_tests: {core_path}/spring-cloud-contract-wiremock
Modules giving you the possibility to use
http://wiremock.org[WireMock] with different servers by using the
@@ -321,6 +323,78 @@ available on the classpath, you can create a stub using WireMock in a
number of different ways, including as described above using
`@AutoConfigureWireMock(stubs="classpath:resource.json")`.
== Generating Contracts using RestDocs
Another thing that can be generated with Spring RestDocs is the Spring Cloud
Contract DSL file and documentation. If you combine that with Spring Cloud
WireMock then you're getting both the contracts and stubs.
TIP: You might wonder why this functionality is in the WireMock module.
Come to think of it, it does make sense since it makes little sense to generate
only contracts and not generate the stubs. That's why we suggest to do both.
Let's imagine the following test:
[source,java]
----
this.mockMvc.perform(post("/foo")
.accept(MediaType.APPLICATION_PDF)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"foo\": 23 }"))
.andExpect(status().isOk())
.andExpect(content().string("bar"))
// first WireMock
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.foo >= 20)]")
.contentType(MediaType.valueOf("application/json"))
.stub("shouldGrantABeerIfOldEnough"))
// then Contract DSL documentation
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
----
This will lead in the creation of the stub as presented in the previous
section, contract will get generated and a documentation file too.
The contract will be called `index.groovy` and look more like this.
[souce,groovy]
----
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method 'POST'
url 'http://localhost:8080/foo'
body('''
{"foo": 23 }
''')
headers {
header('''Accept''', '''application/json''')
header('''Content-Type''', '''application/json''')
header('''Host''', '''localhost:8080''')
header('''Content-Length''', '''12''')
}
}
response {
status 200
body('''
bar
''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
header('''Content-Length''', '''3''')
}
testMatchers {
jsonPath('$[?(@.foo >= 20)]', byType())
}
}
}
----
the generated document (example for Asciidoc) will contain a formatted contract
(the location of this file would be `index/dsl-contract.adoc`).
=== Spring Cloud Contract Verifier
:introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/1.0.x

View File

@@ -1,5 +1,7 @@
:core_path: ../../../..
:doc_samples: {core_path}/samples/wiremock-jetty
:wiremock_tests: {core_path}/spring-cloud-contract-wiremock
Modules giving you the possibility to use
http://wiremock.org[WireMock] with different servers by using the
@@ -246,4 +248,63 @@ and `contentType()` methods to create request matchers, but not both.
On the consumer side, assuming the `resource.json` generated above is
available on the classpath, you can create a stub using WireMock in a
number of different ways, including as described above using
`@AutoConfigureWireMock(stubs="classpath:resource.json")`.
`@AutoConfigureWireMock(stubs="classpath:resource.json")`.
== Generating Contracts using RestDocs
Another thing that can be generated with Spring RestDocs is the Spring Cloud
Contract DSL file and documentation. If you combine that with Spring Cloud
WireMock then you're getting both the contracts and stubs.
TIP: You might wonder why this functionality is in the WireMock module.
Come to think of it, it does make sense since it makes little sense to generate
only contracts and not generate the stubs. That's why we suggest to do both.
Let's imagine the following test:
[source,java]
----
include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java[tags=contract_snippet]
----
This will lead in the creation of the stub as presented in the previous
section, contract will get generated and a documentation file too.
The contract will be called `index.groovy` and look more like this.
[souce,groovy]
----
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method 'POST'
url 'http://localhost:8080/foo'
body('''
{"foo": 23 }
''')
headers {
header('''Accept''', '''application/json''')
header('''Content-Type''', '''application/json''')
header('''Host''', '''localhost:8080''')
header('''Content-Length''', '''12''')
}
}
response {
status 200
body('''
bar
''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
header('''Content-Length''', '''3''')
}
testMatchers {
jsonPath('$[?(@.foo >= 20)]', byType())
}
}
}
----
the generated document (example for Asciidoc) will contain a formatted contract
(the location of this file would be `index/dsl-contract.adoc`).

View File

@@ -248,7 +248,7 @@ class Request extends Common {
@CompileStatic
@EqualsAndHashCode(includeFields = true)
@ToString(includePackage = false)
private class RequestHeaders extends Headers {
class RequestHeaders extends Headers {
@Override
DslProperty matching(String value) {

View File

@@ -155,7 +155,7 @@ class Response extends Common {
@CompileStatic
@EqualsAndHashCode(includeFields = true)
@ToString(includePackage = false)
private class ResponseHeaders extends Headers {
class ResponseHeaders extends Headers {
@Override
DslProperty matching(String value) {

View File

@@ -64,5 +64,10 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,131 @@
package org.springframework.cloud.contract.wiremock.restdocs;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.restdocs.templates.TemplateEngine;
/**
* A {@link org.springframework.restdocs.snippet.Snippet} that documents the Spring Cloud Contract Groovy DSL.
*
* @author Marcin Grzejszczak
* @since 1.0.4
*/
public class ContractDslSnippet extends TemplatedSnippet {
private static final String CONTRACTS_FOLDER = "contracts";
private static final String SNIPPET_NAME = "dsl-contract";
private Map<String, Object> model = new HashMap<>();
/**
* Creates a new {@code ContractDslSnippet} with no additional attributes.
*/
protected ContractDslSnippet() {
this(null);
}
/**
* Creates a new {@code ContractDslSnippet} with the given additional
* {@code attributes} that will be included in the model during template rendering.
*
* @param attributes The additional attributes
*/
protected ContractDslSnippet(Map<String, Object> attributes) {
super(SNIPPET_NAME, attributes);
}
@Override
protected Map<String, Object> createModel(Operation operation) {
return this.model;
}
@Override public void document(Operation operation) throws IOException {
TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes().get(TemplateEngine.class.getName());
String renderedContract = templateEngine.compileTemplate("default-dsl-contract-only")
.render(createModelForContract(operation));
this.model.put("contract", renderedContract);
storeDslContract(operation, renderedContract);
super.document(operation);
}
private void insertResponseModel(Operation operation, Map<String, Object> model) {
OperationResponse response = operation.getResponse();
model.put("response_status", response.getStatus().value());
model.put("response_body_present", response.getContent().length > 0);
model.put("response_body", response.getContentAsString());
HttpHeaders headers = response.getHeaders();
model.put("response_headers_present", !headers.isEmpty());
model.put("response_headers", headers.toSingleValueMap().entrySet());
@SuppressWarnings("unchecked") Set<String> jsonPaths = (Set<String>) operation.getAttributes()
.get("contract.jsonPaths");
model.put("response_json_paths_present", jsonPaths != null && !jsonPaths.isEmpty());
model.put("response_json_paths", jsonPaths(jsonPaths));
}
private Set<JsonPaths> jsonPaths(Set<String> jsonPaths) {
Set<JsonPaths> paths = new HashSet<>();
if (jsonPaths == null) {
return paths;
}
for (String s : jsonPaths) {
paths.add(new JsonPaths(s));
}
return paths;
}
private void insertRequestModel(Operation operation, Map<String, Object> model) {
OperationRequest request = operation.getRequest();
model.put("request_method", request.getMethod());
model.put("request_url", request.getUri());
model.put("request_body_present", request.getContent().length > 0);
model.put("request_body", request.getContentAsString());
HttpHeaders headers = request.getHeaders();
model.put("request_headers_present", !headers.isEmpty());
model.put("request_headers", headers.toSingleValueMap().entrySet());
}
private Map<String, Object> createModelForContract(Operation operation) {
Map<String, Object> modelForContract = new HashMap<>();
insertRequestModel(operation, modelForContract);
insertResponseModel(operation, modelForContract);
return modelForContract;
}
private void storeDslContract(Operation operation, String content)
throws IOException {
RestDocumentationContext context = (RestDocumentationContext) operation
.getAttributes().get(RestDocumentationContext.class.getName());
File output = new File(context.getOutputDirectory(),
CONTRACTS_FOLDER + "/" + operation.getName() + ".groovy");
output.getParentFile().mkdirs();
try (Writer writer = new OutputStreamWriter(new FileOutputStream(output))) {
writer.append(content);
}
}
}
class JsonPaths {
private final String jsonPath;
JsonPaths(String jsonPath) {
this.jsonPath = jsonPath;
}
public String getJsonPath() {
return this.jsonPath;
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.wiremock.restdocs;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Convenience class for setting up RestDocs to generate a {@link org.springframework.restdocs.snippet.Snippet}
* with Spring Cloud Contract DSL. Example usage:
*
* <pre>
* &#64;RunWith(SpringRunner.class)
* &#64;SpringBootTest
* &#64;AutoConfigureRestDocs(outputDir = "target/snippets")
* &#64;AutoConfigureMockMvc
* public class ContractRestDocsApplicationTests {
*
* &#64;Autowired
* private MockMvc mockMvc;
*
* &#64;Test
* public void contextLoads() throws Exception {
* this.mockMvc.perform(post("/foo")
* .accept(MediaType.APPLICATION_PDF)
* .accept(MediaType.APPLICATION_JSON)
* .contentType(MediaType.APPLICATION_JSON)
* .content("{\"foo\": 23 }"))
* .andExpect(status().isOk())
* .andExpect(content().string("bar"))
* // first WireMock
* .andDo(WireMockRestDocs.verify()
* .jsonPath("$[?(&#64;.foo >= 20)]")
* .contentType(MediaType.valueOf("application/json"))
* .stub("shouldGrantABeerIfOldEnough"))
* // then Contract DSL documentation
* .andDo(document("index", SpringCloudContractRestDocs.dslContract()));
* }
* </pre>
*
* which creates a file "target/snippets/contracts/index.groovy" and a
* standard documentation entitled `dsl-contract.adoc` containing that contract.
*
* @author Marcin Grzejszczak
* @since 1.0.4
*/
public class SpringCloudContractRestDocs {
private SpringCloudContractRestDocs() {
}
/**
* Returns a new {@code Snippet} that will document Spring Cloud Contract DSL for the API
* operation.
*
* @return the snippet that will document the Spring Cloud Contract DSL
*/
public static Snippet dslContract() {
return new ContractDslSnippet();
}
/**
* Returns a new {@code Snippet} that will document the Spring Cloud Contract DSL for the API
* operation. The given {@code attributes} will be available during snippet
* generation.
*
* @param attributes the attributes
* @return the snippet that will document the Spring Cloud Contract DSL
*/
public static Snippet dslContract(Map<String, Object> attributes) {
return new ContractDslSnippet(attributes);
}
}

View File

@@ -0,0 +1,42 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method '{{request_method}}'
url '{{request_url}}'
{{#request_body_present}}
body('''
{{request_body}}
''')
{{/request_body_present}}
{{#request_headers_present}}
headers {
{{#request_headers}}
header('''{{key}}''', '''{{value}}''')
{{/request_headers}}
}
{{/request_headers_present}}
}
response {
status {{response_status}}
{{#response_body_present}}
body('''
{{response_body}}
''')
{{/response_body_present}}
{{#response_headers_present}}
headers {
{{#response_headers}}
header('''{{key}}''', '''{{value}}''')
{{/response_headers}}
}
{{/response_headers_present}}
{{#response_json_paths_present}}
testMatchers {
{{#response_json_paths}}
jsonPath('{{jsonPath}}', byType())
{{/response_json_paths}}
}
{{/response_json_paths_present}}
}
}

View File

@@ -0,0 +1,137 @@
package org.springframework.cloud.contract.wiremock.restdocs;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.cloud.contract.wiremock.restdocs.SpringCloudContractRestDocs.dslContract;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Marcin Grzejszczak
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ContractDslSnippetTests.Config.class)
public class ContractDslSnippetTests {
private static final String OUTPUT = "target/generated-snippets";
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
MockMvc mockMvc;
@Autowired WebApplicationContext context;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.build();
}
@Test
public void should_create_contract_template_and_doc() throws Exception {
//tag::contract_snippet[]
this.mockMvc.perform(post("/foo")
.accept(MediaType.APPLICATION_PDF)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"foo\": 23 }"))
.andExpect(status().isOk())
.andExpect(content().string("bar"))
// first WireMock
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.foo >= 20)]")
.contentType(MediaType.valueOf("application/json"))
.stub("shouldGrantABeerIfOldEnough"))
// then Contract DSL documentation
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
//end::contract_snippet[]
then(file("/contracts/index.groovy")).exists();
then(file("/index/dsl-contract.adoc")).exists();
String contract = readFromFile(file("/contracts/index.groovy"));
// try to parse the contract
Contract parsedContract = ContractVerifierDslConverter.convert(contract);
then(parsedContract.getRequest().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getRequest().getMethod().getClientValue()).isNotNull();
then(parsedContract.getRequest().getUrl().getClientValue()).isNotNull();
then(parsedContract.getRequest().getBody().getClientValue()).isNotNull();
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getResponse().getBody().getClientValue()).isNotNull();
then(parsedContract.getResponse().getMatchers().hasMatchers()).isTrue();
}
@Test
public void should_create_contract_template_and_doc_without_body_and_headers() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/foo"))
.andExpect(status().isOk())
.andDo(document("empty", dslContract()));
then(file("/contracts/empty.groovy")).exists();
then(file("/empty/dsl-contract.adoc")).exists();
String contract = readFromFile(file("/contracts/empty.groovy"));
// try to parse the contract
Contract parsedContract = ContractVerifierDslConverter.convert(contract);
then(parsedContract.getRequest().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getRequest().getMethod().getClientValue()).isNotNull();
then(parsedContract.getRequest().getUrl().getClientValue()).isNotNull();
then(parsedContract.getRequest().getBody()).isNull();
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders()).isNull();
then(parsedContract.getResponse().getBody()).isNull();
then(parsedContract.getResponse().getMatchers()).isNull();
}
private File file(String name) throws URISyntaxException {
return new File(OUTPUT, name);
}
private String readFromFile(File f) throws IOException {
byte[] encoded = Files.readAllBytes(f.toPath());
return new String(encoded, StandardCharsets.UTF_8);
}
@Configuration
@EnableAutoConfiguration
@RestController
static class Config {
@PostMapping("/foo")
String foo() {
return "bar";
}
@GetMapping("/foo")
void getFoo() {
}
}
}