diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 524f3afa..64830cd9 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -11,11 +11,36 @@ static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`: [source,java,indent=0] ---- -include::{examples-dir}/com/example/Preprocessing.java[tags=general] +include::{examples-dir}/com/example/PreprocessingPerTest.java[tags=preprocessing] ---- <1> Apply a request preprocessor that will remove the header named `Foo`. <2> Apply a response preprocessor that will pretty print its content. +Alternatively, you may want to apply the same preprocessors to every test. You can do +so by configuring the preprocessors in your `@Before` method and using the +<>: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=setup] +---- +<1> Create the `RestDocumentationResultHandler`, configured to preprocess the request + and response. +<2> Create the `MockMvc` instance, configured to always call the documentation result + handler. + +Then, in each test, the `RestDocumentationResultHandler` can be configured with anything +test-specific. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=use] +---- +<1> Document the links specific to the resource that is being tested +<2> The `perform` call will automatically produce the documentation snippets due to the + use of `alwaysDo` above. + Various built in preprocessors, including those illustrated above, are available via the static methods on `Preprocessors`. See below for further details. diff --git a/docs/src/test/java/com/example/PreprocessingEveryTest.java b/docs/src/test/java/com/example/PreprocessingEveryTest.java new file mode 100644 index 00000000..6199ecb4 --- /dev/null +++ b/docs/src/test/java/com/example/PreprocessingEveryTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014-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 com.example; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; + +import org.junit.Before; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class PreprocessingEveryTest { + + private WebApplicationContext context; + + private MockMvc mockMvc; + + private RestDocumentationResultHandler document; + + // tag::setup[] + @Before + public void setup() { + this.document = document("{method-name}", // <1> + preprocessRequest(removeHeaders("Foo")), + preprocessResponse(prettyPrint())); + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .alwaysDo(this.document) // <2> + .build(); + } + // end::setup[] + + public void use() throws Exception { + // tag::use[] + this.document.snippets( // <1> + links(linkWithRel("self").description("Canonical self link"))); + this.mockMvc.perform(get("/")) // <2> + .andExpect(status().isOk()); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/Preprocessing.java b/docs/src/test/java/com/example/PreprocessingPerTest.java similarity index 94% rename from docs/src/test/java/com/example/Preprocessing.java rename to docs/src/test/java/com/example/PreprocessingPerTest.java index dbb83713..6f0ab77f 100644 --- a/docs/src/test/java/com/example/Preprocessing.java +++ b/docs/src/test/java/com/example/PreprocessingPerTest.java @@ -26,18 +26,18 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr import org.springframework.test.web.servlet.MockMvc; -public class Preprocessing { +public class PreprocessingPerTest { private MockMvc mockMvc; public void general() throws Exception { - // tag::general[] + // tag::preprocessing[] this.mockMvc.perform(get("/")) .andExpect(status().isOk()) .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> preprocessResponse(prettyPrint()))); // <2> - // end::general[] + // end::preprocessing[] } } diff --git a/samples/rest-notes-spring-hateoas/src/main/resources/application.properties b/samples/rest-notes-spring-hateoas/src/main/resources/application.properties deleted file mode 100644 index 8e06a828..00000000 --- a/samples/rest-notes-spring-hateoas/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 8a0978df..81a7fef7 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -18,13 +18,16 @@ package com.example.notes; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -48,6 +51,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -66,6 +70,8 @@ public class ApiDocumentation { @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + + private RestDocumentationResultHandler document; @Autowired private NoteRepository noteRepository; @@ -83,12 +89,25 @@ public class ApiDocumentation { @Before public void setUp() { + this.document = document("{method-name}", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint())); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(this.document) + .build(); } @Test public void errorExample() throws Exception { + this.document.snippets(responseFields( + fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), + fieldWithPath("message").description("A description of the cause of the error"), + fieldWithPath("path").description("The path to which the request was made"), + fieldWithPath("status").description("The HTTP status code, e.g. `400`"), + fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))); + this.mockMvc .perform(get("/error") .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) @@ -100,26 +119,20 @@ public class ApiDocumentation { .andExpect(jsonPath("error", is("Bad Request"))) .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(document("error-example", - responseFields( - fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), - fieldWithPath("message").description("A description of the cause of the error"), - fieldWithPath("path").description("The path to which the request was made"), - fieldWithPath("status").description("The HTTP status code, e.g. `400`"), - fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred")))); + .andExpect(jsonPath("path", is(notNullValue()))); } @Test public void indexExample() throws Exception { + this.document.snippets( + links( + linkWithRel("notes").description("The <>"), + linkWithRel("tags").description("The <>")), + responseFields( + fieldWithPath("_links").description("<> to other resources"))); + this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index-example", - links( - linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>")), - responseFields( - fieldWithPath("_links").description("<> to other resources")))); + .andExpect(status().isOk()); } @Test @@ -131,12 +144,13 @@ public class ApiDocumentation { createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); + + this.document.snippets( + responseFields( + fieldWithPath("_embedded.notes").description("An array of <>"))); this.mockMvc.perform(get("/notes")) - .andExpect(status().isOk()) - .andDo(document("notes-list-example", - responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); + .andExpect(status().isOk()); } @Test @@ -157,16 +171,17 @@ public class ApiDocumentation { note.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NoteInput.class); + + this.document.snippets( + requestFields( + fields.withPath("title").description("The title of the note"), + fields.withPath("body").description("The body of the note"), + fields.withPath("tags").description("An array of tag resource URIs"))); this.mockMvc.perform( post("/notes").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andDo(document("notes-create-example", - requestFields( - fields.withPath("title").description("The title of the note"), - fields.withPath("body").description("The body of the note"), - fields.withPath("tags").description("An array of tag resource URIs")))); + .andExpect(status().isCreated()); } @Test @@ -192,21 +207,22 @@ public class ApiDocumentation { this.objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); + + this.document.snippets( + links( + linkWithRel("self").description("This <>"), + linkWithRel("note-tags").description("This note's <>")), + responseFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("_links").description("<> to other resources"))); this.mockMvc.perform(get(noteLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("title", is(note.get("title")))) .andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDo(document("note-get-example", - links( - linkWithRel("self").description("This <>"), - linkWithRel("note-tags").description("This note's <>")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); } @@ -218,12 +234,13 @@ public class ApiDocumentation { createTag("REST"); createTag("Hypermedia"); createTag("HTTP"); + + this.document.snippets( + responseFields( + fieldWithPath("_embedded.tags").description("An array of <>"))); this.mockMvc.perform(get("/tags")) - .andExpect(status().isOk()) - .andDo(document("tags-list-example", - responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); + .andExpect(status().isOk()); } @Test @@ -232,14 +249,15 @@ public class ApiDocumentation { tag.put("name", "REST"); ConstrainedFields fields = new ConstrainedFields(TagInput.class); + + this.document.snippets( + requestFields( + fields.withPath("name").description("The name of the tag"))); this.mockMvc.perform( post("/tags").contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andDo(document("tags-create-example", - requestFields( - fields.withPath("name").description("The name of the tag")))); + .andExpect(status().isCreated()); } @Test @@ -275,23 +293,24 @@ public class ApiDocumentation { noteUpdate.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class); + + this.document.snippets( + requestFields( + fields.withPath("title") + .description("The title of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("body") + .description("The body of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("tags") + .description("An array of tag resource URIs"))); this.mockMvc.perform( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("note-update-example", - requestFields( - fields.withPath("title") - .description("The title of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("body") - .description("The body of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("tags") - .description("An array of tag resource URIs")))); + .andExpect(status().isNoContent()); } @Test @@ -305,17 +324,18 @@ public class ApiDocumentation { this.objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()).andReturn().getResponse() .getHeader("Location"); + + this.document.snippets( + links( + linkWithRel("self").description("This <>"), + linkWithRel("tagged-notes").description("The <> that have this tag")), + responseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("<> to other resources"))); this.mockMvc.perform(get(tagLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example", - links( - linkWithRel("self").description("This <>"), - linkWithRel("tagged-notes").description("The <> that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); + .andExpect(jsonPath("name", is(tag.get("name")))); } @Test @@ -334,15 +354,15 @@ public class ApiDocumentation { tagUpdate.put("name", "RESTful"); ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); + + this.document.snippets( + requestFields( + fields.withPath("name").description("The name of the tag"))); this.mockMvc.perform( patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("tag-update-example", - requestFields( - fields.withPath("name") - .description("The name of the tag")))); + .andExpect(status().isNoContent()); } private void createNote(String title, String body) { diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index b1365729..60ba0969 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,11 +19,14 @@ 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.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -72,7 +75,9 @@ public class GettingStartedDocumentation { public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/")) + .alwaysDo(document("{method-name}/{step}/", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()))) .build(); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 2a7e4ab4..32834c9e 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -79,7 +79,7 @@ public class RestDocumentationResultHandler implements ResultHandler { this.identifier = identifier; this.requestPreprocessor = requestPreprocessor; this.responsePreprocessor = responsePreprocessor; - this.snippets = Arrays.asList(snippets); + this.snippets = new ArrayList<>(Arrays.asList(snippets)); } @Override @@ -102,11 +102,23 @@ public class RestDocumentationResultHandler implements ResultHandler { } } + /** + * Adds the given {@code snippets} such that that are documented when this result + * handler is called. + * + * @param snippets the snippets to add + * @return this {@code ResultDocumentationResultHandler} + */ + public RestDocumentationResultHandler snippets(Snippet... snippets) { + this.snippets.addAll(Arrays.asList(snippets)); + return this; + } + @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { List combinedSnippets = new ArrayList<>((List) result - .getRequest() - .getAttribute("org.springframework.restdocs.defaultSnippets")); + .getRequest().getAttribute( + "org.springframework.restdocs.mockmvc.defaultSnippets")); combinedSnippets.addAll(this.snippets); return combinedSnippets; } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java index ace722ea..ba874f42 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java @@ -64,7 +64,7 @@ public class SnippetConfigurer extends void apply(MockHttpServletRequest request) { ((WriterResolver) request.getAttribute(WriterResolver.class.getName())) .setEncoding(this.snippetEncoding); - request.setAttribute("org.springframework.restdocs.defaultSnippets", + request.setAttribute("org.springframework.restdocs.mockmvc.defaultSnippets", this.defaultSnippets); }