Remove CGLib proxy “magic” in favour of an explicit document method

Previously, the name of an output file was automatically determined
by the name of the method from which the mockMvc.perform call was made.
While (somewhat) clever, to support this for non @Test methods, this
required the use of a CGLib proxy to push and pop some context that
kept track of the name of the current method. This meant it only worked
for non-private methods. It also made it hard to look at the code and
see what would and would not be documented.

This commit updates the library to provide an explicit document method
instead. This method takes the path of an output directory and a
MockMvc ResultActions instance, typically returned from a call to
mockMvc.perform. For example:

document("index",
        this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
		.andExpect(status().isOk()));

This will perform a GET request to "/", assert that the response is
200 OK and write documentation snippets for the request and response
to a directory named index.
This commit is contained in:
Andy Wilkinson
2014-10-28 14:57:34 +00:00
parent aa81f348d7
commit 01e37f1e4f
10 changed files with 262 additions and 352 deletions

View File

@@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar
You can check that the service is up and running by executing a simple request using
cURL:
include::{generated}/GettingStartedDocumentation/indexRequest.asciidoc[]
include::{generated}/index/request.asciidoc[]
This request should yield the following response in the
http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format:
include::{generated}/GettingStartedDocumentation/indexResponse.asciidoc[]
include::{generated}/index/response.asciidoc[]
Note the `_links` in the JSON response. They are key to navigating the API.
@@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is
it to create a new note. As you saw above, the URI for working with notes is included as
a link when you perform a `GET` request against the root of the service:
include::{generated}/GettingStartedDocumentation/indexResponse.asciidoc[]
include::{generated}/index/response.asciidoc[]
To create a note, you need to execute a `POST` request to this URI including a JSON
payload containing the title and body of the note:
include::{generated}/GettingStartedDocumentation/createNoteRequest.asciidoc[]
include::{generated}/create-note/request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created note:
include::{generated}/GettingStartedDocumentation/createNoteResponse.asciidoc[]
include::{generated}/create-note/response.asciidoc[]
To work with the newly created note you use the URI in the `Location` header. For example,
you can access the note's details by performing a `GET` request:
include::{generated}/GettingStartedDocumentation/getNoteRequest.asciidoc[]
include::{generated}/get-note/request.asciidoc[]
This request will produce a response with the note's details in its body:
include::{generated}/GettingStartedDocumentation/getNoteResponse.asciidoc[]
include::{generated}/get-note/response.asciidoc[]
Note the `tags` link which we'll make use of later.
@@ -92,26 +92,26 @@ to tag a note, you must first create the tag.
Referring back to the response for the service's index, the URI for working with tags is
include as a link:
include::{generated}/GettingStartedDocumentation/indexResponse.asciidoc[]
include::{generated}/index/response.asciidoc[]
To create a tag you need to execute a `POST` request to this URI, including a JSON
payload containing the name of the tag:
include::{generated}/GettingStartedDocumentation/createTagRequest.asciidoc[]
include::{generated}/create-tag/request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created tag:
include::{generated}/GettingStartedDocumentation/createTagResponse.asciidoc[]
include::{generated}/create-tag/response.asciidoc[]
To work with the newly created tag you use the URI in the `Location` header. For example
you can access the tag's details by performing a `GET` request:
include::{generated}/GettingStartedDocumentation/getTagRequest.asciidoc[]
include::{generated}/get-tag/request.asciidoc[]
This request will produce a response with the tag's details in its body:
include::{generated}/GettingStartedDocumentation/getTagResponse.asciidoc[]
include::{generated}/get-tag/response.asciidoc[]
@@ -132,24 +132,24 @@ with it.
Once again we execute a `POST` request. However, this time, in an array named tags, we
include the URI of the tag we just created:
include::{generated}/GettingStartedDocumentation/createTaggedNoteRequest.asciidoc[]
include::{generated}/create-tagged-note/request.asciidoc[]
Once again, the response's `Location` header tells us the URI of the newly created note:
include::{generated}/GettingStartedDocumentation/createTaggedNoteResponse.asciidoc[]
include::{generated}/create-tagged-note/response.asciidoc[]
As before, a `GET` request executed against this URI will retrieve the note's details:
include::{generated}/GettingStartedDocumentation/getTaggedNoteRequestResponse.asciidoc[]
include::{generated}/get-tagged-note/request-response.asciidoc[]
To verify that the tag has been associated with the note, we can perform a `GET` request
against the URI from the `tags` link:
include::{generated}/GettingStartedDocumentation/getTagsRequest.asciidoc[]
include::{generated}/get-tags/request.asciidoc[]
The response embeds information about the tag that we've just associated with the note:
include::{generated}/GettingStartedDocumentation/getTagsResponse.asciidoc[]
include::{generated}/get-tags/response.asciidoc[]
@@ -159,17 +159,17 @@ An existing note can be tagged by executing a `PATCH` request against the note's
a body that contains the array of tags to be associated with the note. We'll used the
URI of the untagged note that we created earlier:
include::{generated}/GettingStartedDocumentation/tagExistingNoteRequest.asciidoc[]
include::{generated}/tag-existing-note/request.asciidoc[]
This request should produce a `204 No Content` response:
include::{generated}/GettingStartedDocumentation/tagExistingNoteResponse.asciidoc[]
include::{generated}/tag-existing-note/response.asciidoc[]
When we first created this note, we noted the tags link included in its details:
include::{generated}/GettingStartedDocumentation/createNoteResponse.asciidoc[]
include::{generated}/get-note/response.asciidoc[]
We can use that link now and execute a `GET` request to see that the note now has a
single tag:
include::{generated}/GettingStartedDocumentation/getTagsForExistingNoteRequestResponse.asciidoc[]
include::{generated}/get-tags-for-existing-note/request-response.asciidoc[]

View File

@@ -19,6 +19,7 @@ package com.example.notes;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.restdocs.core.RestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -38,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.restdocs.core.RestDocumentationConfiguration;
import org.springframework.restdocs.core.RestDocumentationJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@@ -49,7 +50,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
@RunWith(RestDocumentationJUnit4ClassRunner.class)
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class GettingStartedDocumentation {
@@ -70,10 +71,12 @@ public class GettingStartedDocumentation {
@Test
public void index() throws Exception {
this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.notes", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue())));
document(
"index",
this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.notes", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue()))));
}
@Test
@@ -98,41 +101,49 @@ public class GettingStartedDocumentation {
note.put("title", "Note creation with cURL");
note.put("body", "An example of how to create a note using cURL");
String noteLocation = this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())).andReturn()
.getResponse().getHeader("Location");
String noteLocation = document(
"create-note",
this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())))
.andReturn().getResponse().getHeader("Location");
return noteLocation;
}
void getNote(String noteLocation) throws Exception {
this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue())));
document(
"get-note",
this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue()))));
}
String createTag() throws Exception, JsonProcessingException {
Map<String, String> tag = new HashMap<String, String>();
tag.put("name", "getting-started");
String tagLocation = this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())).andReturn()
.getResponse().getHeader("Location");
String tagLocation = document(
"create-tag",
this.mockMvc
.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())))
.andReturn().getResponse().getHeader("Location");
return tagLocation;
}
void getTag(String tagLocation) throws Exception {
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
.andExpect(jsonPath("name", is(notNullValue())))
.andExpect(jsonPath("_links.notes", is(notNullValue())));
document(
"get-tag",
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
.andExpect(jsonPath("name", is(notNullValue())))
.andExpect(jsonPath("_links.notes", is(notNullValue()))));
}
String createTaggedNote(String tag) throws Exception {
@@ -141,50 +152,59 @@ public class GettingStartedDocumentation {
note.put("body", "An example of how to create a tagged note using cURL");
note.put("tags", Arrays.asList(tag));
String noteLocation = this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())).andReturn()
.getResponse().getHeader("Location");
String noteLocation = document(
"create-tagged-note",
this.mockMvc
.perform(
post("/notes").contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", notNullValue())))
.andReturn().getResponse().getHeader("Location");
return noteLocation;
}
void getTaggedNote(String tagLocation) throws Exception {
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue())));
document(
"get-tagged-note",
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk())
.andExpect(jsonPath("title", is(notNullValue())))
.andExpect(jsonPath("body", is(notNullValue())))
.andExpect(jsonPath("_links.tags", is(notNullValue()))));
}
void getTags(String taggedNoteLocation) throws Exception {
String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation))
.andReturn(), "tags");
this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1)));
document("get-tags",
this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1))));
}
void tagExistingNote(String noteLocation, String tagLocation) throws Exception {
Map<String, Object> update = new HashMap<String, Object>();
update.put("tags", Arrays.asList(tagLocation));
this.mockMvc.perform(
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(update))).andExpect(
status().isNoContent());
document(
"tag-existing-note",
this.mockMvc.perform(
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
objectMapper.writeValueAsString(update))).andExpect(
status().isNoContent()));
}
void getTaggedExistingNote(String tagLocation) throws Exception {
this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk());
document("get-tagged-existing-note", this.mockMvc.perform(get(tagLocation))
.andExpect(status().isOk()));
}
void getTagsForExistingNote(String taggedNoteLocation) throws Exception {
String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation))
.andReturn(), "tags");
this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1)));
document("get-tags-for-existing-note",
this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk())
.andExpect(jsonPath("_embedded.tags", hasSize(1))));
}
private String getLink(MvcResult result, String href)