Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use URI templates to generate stubs with urlPattern #63

Merged
merged 3 commits into from
Mar 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
!.travis.yml
.settings/
build/
out/
bin/
gradle.properties
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ artifact repository for this purpose.

Details and background information can be read on our [ePages Developer Blog](https://developer.epages.com/blog/2016/07/14/wiremock.html).

<!-- TOC depthFrom:2 -->

- [Contents](#contents)
- [How to include `restdocs-wiremock` into your server project](#how-to-include-restdocs-wiremock-into-your-server-project)
- [Dependencies](#dependencies)
- [Producing snippets](#producing-snippets)
- [The WireMock stubs jar](#the-wiremock-stubs-jar)
- [How to use WireMock in your client tests](#how-to-use-wiremock-in-your-client-tests)
- [Dependencies](#dependencies-1)
- [Configuring your test to use the WireMock stubs](#configuring-your-test-to-use-the-wiremock-stubs)
- [Building from source](#building-from-source)
- [Publishing](#publishing)
- [Other resources](#other-resources)

<!-- /TOC -->
## Contents

This repository consists of four projects
Expand Down Expand Up @@ -84,7 +99,7 @@ to the `document()` calls for Spring REST Docs. For example:

```java
@RunWith(SpringJUnit4ClassRunner.class)
...
//...
class ApiDocumentation {
// ... the usual test setup.
void testGetSingleNote() {
Expand Down Expand Up @@ -118,6 +133,42 @@ the response body as provided by the integration test.
}
```

The above snippet has a shortcoming. WireMock will only match a request if the url matches the complete path, including the id.
This is really inflexible.
We can do better by using `RestDocumentationRequestBuilders` to construct the request using a url template, instead of `MockMvcRequestBuilders`.

```java
@RunWith(SpringJUnit4ClassRunner.class)
//...
class ApiDocumentation {
// ... the usual test setup.
void testGetSingleNote() {
this.mockMvc.perform(RestDocumentationRequestBuilders.get("/notes/{id}", 1)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("get-note",
wiremockJson(),
responseFields( ... )
));
}
}
```

This generates a snippet that uses `urlPattern` instead if `urlPath`.
So WireMock would match a request with any id value.

```json
{
"request" : {
"method" : "GET",
"urlPattern" : "/notes/[^/]+"
},
"response" : {
//...
}
}
```

### The WireMock stubs jar

On the server side you need to collect the WireMock stubs and publish them into an artifact repository.
Expand Down
5 changes: 3 additions & 2 deletions server/src/test/java/com/example/notes/ApiDocumentation.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.springframework.hateoas.MediaTypes;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.constraints.ConstraintDescriptions;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
Expand Down Expand Up @@ -219,7 +220,7 @@ public void noteGetExample() throws Exception {
.andReturn().getResponse().getHeader("Location");

this.mockMvc
.perform(get(noteLocation))
.perform(RestDocumentationRequestBuilders.get("/notes/{id}", noteLocation.substring(noteLocation.lastIndexOf("/") + 1)))
.andExpect(status().isOk())
.andExpect(jsonPath("title", is(note.get("title"))))
.andExpect(jsonPath("body", is(note.get("body"))))
Expand Down Expand Up @@ -338,7 +339,7 @@ public void tagGetExample() throws Exception {
.andReturn().getResponse().getHeader("Location");

this.mockMvc
.perform(get(tagLocation))
.perform(RestDocumentationRequestBuilders.get("/tags/{id}", tagLocation.substring(tagLocation.lastIndexOf("/") + 1)))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(tag.get("name"))))
.andDo(this.documentationHandler.document(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,6 @@ void tagExistingNote(String noteLocation, String tagLocation) throws Exception {
.andExpect(status().isNoContent());
}

MvcResult getTaggedExistingNote(String noteLocation) throws Exception {
return this.mockMvc.perform(get(noteLocation))
.andExpect(status().isOk())
.andReturn();
}

void getTagsForExistingNote(String noteTagsLocation) throws Exception {
this.mockMvc.perform(get(noteTagsLocation))
.andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1)));
}

private String getLink(MvcResult result, String rel)
throws UnsupportedEncodingException {
return JsonPath.parse(result.getResponse().getContentAsString()).read(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.epages.restdocs;

import static org.springframework.restdocs.generate.RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
Expand All @@ -17,6 +19,9 @@
import org.springframework.restdocs.snippet.StandardWriterResolver;
import org.springframework.restdocs.snippet.WriterResolver;
import org.springframework.restdocs.templates.TemplateFormat;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -61,8 +66,9 @@ protected Map<Object, Object> createModel(Operation operation) {
OperationResponse response = operation.getResponse();

Maps.Builder<Object, Object> requestBuilder = Maps.builder()
.put("method", operation.getRequest().getMethod())
.put("urlPath", operation.getRequest().getUri().getRawPath());
.put("method", operation.getRequest().getMethod());

urlPathOrUrlPattern(operation, requestBuilder);

Maps.Builder<Object, Object> responseBuilder = Maps.builder()
.put("status", response.getStatus().value()).put("headers", responseHeaders(response))
Expand All @@ -82,6 +88,32 @@ protected Map<Object, Object> createModel(Operation operation) {
.build();
}

/**
* If ATTRIBUTE_NAME_URL_TEMPLATE is present use it to build a urlPattern instead of a urlPath.
*
* This allows for more flexible request matching when the path contains variable elements.
*
* ATTRIBUTE_NAME_URL_TEMPLATE is present if the urlTemplate factore methods of RestDocumentationRequestBuilders are used.
*
* @param operation
* @param requestBuilder
*/
private void urlPathOrUrlPattern(Operation operation, Maps.Builder<Object, Object> requestBuilder) {
String urlTemplate = (String) operation.getAttributes().get(ATTRIBUTE_NAME_URL_TEMPLATE);
if (StringUtils.isEmpty(urlTemplate)) {
requestBuilder.put("urlPath", operation.getRequest().getUri().getRawPath());
} else {
UriTemplate uriTemplate = new UriTemplate(urlTemplate);
UriComponentsBuilder uriTemplateBuilder = UriComponentsBuilder.fromUriString(urlTemplate);
Maps.Builder<String, String> uriVariables = Maps.builder();
for (String variableName : uriTemplate.getVariableNames()) {
uriVariables.put(variableName, "[^/]+");
}
String uriPathRegex = uriTemplateBuilder.buildAndExpand(uriVariables.build()).getPath();
requestBuilder.put("urlPattern", uriPathRegex);
}
}

private Map<Object, Object> responseHeaders(OperationResponse response) {
Maps.Builder<Object, Object> responseHeaders = Maps.builder();
for (Map.Entry<String, List<String>> e : response.getHeaders().entrySet()) {
Expand Down Expand Up @@ -168,4 +200,4 @@ private Map<K, V> build() {
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.restdocs.generate.RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;

import java.io.IOException;
import java.net.URI;
Expand Down Expand Up @@ -36,8 +38,6 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;

import uk.co.datumedge.hamcrest.json.SameJSONAs;

public class WireMockJsonSnippetTest {

private static final TemplateFormat FORMAT = WireMockJsonSnippet.TEMPLATE_FORMAT;
Expand Down Expand Up @@ -76,8 +76,8 @@ public void basicHandling() throws IOException {
@Test
public void simpleRequest() throws IOException {
this.expectedSnippet.expectWireMockJson("simple-request").withContents(
(Matcher<String>) SameJSONAs
.sameJSONAs(new ObjectMapper().writeValueAsString(expectedJsonForSimpleRequest())));
(Matcher<String>)
sameJSONAs(new ObjectMapper().writeValueAsString(expectedJsonForSimpleRequest())));
wiremockJson().document(operationBuilder("simple-request").request("http://localhost/").method("GET").build());
}

Expand All @@ -89,11 +89,31 @@ public void simpleRequest() throws IOException {
of("headers", emptyMap(), "body", "", "status", 200));
}

@SuppressWarnings("unchecked")
@Test
public void simpleRequestWithUriTemplate() throws IOException {
this.expectedSnippet.expectWireMockJson("simple-request").withContents(
(Matcher<String>)
sameJSONAs(new ObjectMapper().writeValueAsString(expectedJsonForSimpleRequestWithUrlPattern())));
wiremockJson().document(operationBuilder("simple-request")
.attribute(ATTRIBUTE_NAME_URL_TEMPLATE, "http://localhost/some/{id}/other")
.request("http://localhost/some/123-qbc/other")
.method("GET").build());
}

private ImmutableMap<String, ImmutableMap<String, ? extends Object>> expectedJsonForSimpleRequestWithUrlPattern() {
return of( //
"request", //
of("method", "GET", "urlPattern", "/some/[^/]+/other"), //
"response", //
of("headers", emptyMap(), "body", "", "status", 200));
}

@SuppressWarnings("unchecked")
@Test
public void getRequestWithParams() throws IOException {
this.expectedSnippet.expectWireMockJson("get-request").withContents(
(Matcher<String>) SameJSONAs.sameJSONAs(new ObjectMapper().writeValueAsString(
(Matcher<String>) sameJSONAs(new ObjectMapper().writeValueAsString(
of( //
"request", //
of("method", "GET", "urlPath", "/foo", "queryParameters", //
Expand All @@ -108,8 +128,8 @@ public void getRequestWithParams() throws IOException {
@SuppressWarnings("unchecked")
@Test
public void postRequest() throws IOException {
this.expectedSnippet.expectWireMockJson("post-request").withContents((Matcher<String>) SameJSONAs
.sameJSONAs(new ObjectMapper().writeValueAsString(
this.expectedSnippet.expectWireMockJson("post-request").withContents((Matcher<String>)
sameJSONAs(new ObjectMapper().writeValueAsString(
of( //
"request", //
of("method", "POST", "urlPath", "/", "headers", of("Content-Type", of("contains", "uri-list"))), //
Expand All @@ -126,8 +146,8 @@ public void postRequest() throws IOException {
@Test
@SuppressWarnings("unchecked")
public void customMediaType() throws IOException {
this.expectedSnippet.expectWireMockJson("custom-mediatype").withContents((Matcher<String>) SameJSONAs
.sameJSONAs(new ObjectMapper().writeValueAsString(
this.expectedSnippet.expectWireMockJson("custom-mediatype").withContents((Matcher<String>)
sameJSONAs(new ObjectMapper().writeValueAsString(
of( //
"request", //
of("method", "GET", "urlPath", "/foo",
Expand Down