From 1eeb60510828f1405ad784df7b58f3148b601704 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Feb 2015 10:56:01 +0000 Subject: [PATCH] Support using alwaysDo to automatically document every MockMvc call The MVC test framework provides an alwaysDo method which allows the configuration of a result handler to be that will be invoked for every call to perform. Previously, this method could not be used for producing documentation snippets as the snippets would overwrite each other. This commit adds support for writing snippets to a parameterized output directory. Two parameters are supported: method name and step. Method name is the name of the currently executing test method. Step is a count of the number of calls to MockMvc.perform that have been made in that method. Closes gh-14 --- README.md | 33 ++++ build.gradle | 2 + .../asciidoc/getting-started-guide.asciidoc | 42 ++--- .../src/main/resources/application.properties | 1 + .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 95 ++++------- .../asciidoc/getting-started-guide.asciidoc | 44 ++--- .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 72 +++----- .../RestDocumentationConfigurer.java | 7 +- .../config/RestDocumentationContext.java | 88 ++++++++++ ...estDocumentationTestExecutionListener.java | 40 +++++ .../restdocs/snippet/OutputFileResolver.java | 101 ++++++++++++ .../snippet/SnippetWritingResultHandler.java | 18 +- .../main/resources/META-INF/spring.factories | 1 + .../RestDocumentationIntegrationTests.java | 141 ++++++++++++++++ .../RestDocumentationConfigurerTests.java | 4 +- .../snippet/OutputFileResolverTests.java | 155 ++++++++++++++++++ 18 files changed, 681 insertions(+), 167 deletions(-) create mode 100644 samples/rest-notes-spring-data-rest/src/main/resources/application.properties rename spring-restdocs/src/main/java/org/springframework/restdocs/{ => config}/RestDocumentationConfigurer.java (93%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java create mode 100644 spring-restdocs/src/main/resources/META-INF/spring.factories create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java rename spring-restdocs/src/test/java/org/springframework/restdocs/{ => config}/RestDocumentationConfigurerTests.java (95%) create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java diff --git a/README.md b/README.md index e86e47bb..9bacefee 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,39 @@ be written: - `index/response.asciidoc` - `index/request-response.asciidoc` +#### Parameterized output directories + +The `document` method supports parameterized output directories. The following parameters +are supported: + +| Parameter | Description +| ------------- | ----------- +| {methodName} | The name of the test method, formatted using camelcase +| {method-name} | The name of the test method, formatted with dash separators +| {method_name} | The name of the test method, formatted with underscore separators +| {step} | The count of calls to `MockMvc.perform` in the current test + +For example, `document("{method-name}")` in a test method named `creatingANote` will +write snippets into a directory named `creating-a-note`. + +The `{step}` parameter is particularly useful in combination with Spring MVC Test's +`alwaysDo` functionality. It allows documentation to be configured once in a setup method: + +```java +@Before +public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); +} +``` + +With this configuration in place, every call to `MockMvc.perform` will produce +documentation snippets without any further configuration. Take a look at the +`GettingStartedDocumentation` classes in each of the sample applications to see this +functionality in action. + #### Pretty-printed snippets To improve the readability of the generated snippets you may want to configure your diff --git a/build.gradle b/build.gradle index 18ac9bd3..380f5559 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ project(':spring-restdocs') { junitVersion = '4.11' servletApiVersion = '3.1.0' springVersion = '4.1.4.RELEASE' + mockitoVersion = '1.10.19' } group = 'org.springframework.restdocs' @@ -63,6 +64,7 @@ project(':spring-restdocs') { compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" testCompile "org.springframework:spring-webmvc:$springVersion" + testCompile "org.mockito:mockito-core:$mockitoVersion" } test { diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc index 5f8e71db..e7781f24 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc @@ -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}/index/request.asciidoc[] +include::{generated}/index/1/request.asciidoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/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}/index/response.asciidoc[] +include::{generated}/index/1/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}/create-note/request.asciidoc[] +include::{generated}/creating-a-note/1/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}/create-note/response.asciidoc[] +include::{generated}/creating-a-note/1/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}/get-note/request.asciidoc[] +include::{generated}/creating-a-note/2/request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/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}/index/response.asciidoc[] +include::{generated}/index/1/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}/create-tag/request.asciidoc[] +include::{generated}/creating-a-note/3/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}/create-tag/response.asciidoc[] +include::{generated}/creating-a-note/3/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}/get-tag/request.asciidoc[] +include::{generated}/creating-a-note/4/request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/get-tag/response.asciidoc[] +include::{generated}/creating-a-note/4/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}/create-tagged-note/request.asciidoc[] +include::{generated}/creating-a-note/5/request.asciidoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/create-tagged-note/response.asciidoc[] +include::{generated}/creating-a-note/5/response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/get-tagged-note/request-response.asciidoc[] +include::{generated}/creating-a-note/6/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}/get-tags/request.asciidoc[] +include::{generated}/creating-a-note/7/request.asciidoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/get-tags/response.asciidoc[] +include::{generated}/creating-a-note/7/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}/tag-existing-note/request.asciidoc[] +include::{generated}/creating-a-note/8/request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/tag-existing-note/response.asciidoc[] +include::{generated}/creating-a-note/8/response.asciidoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/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}/get-tags-for-existing-note/request-response.asciidoc[] +include::{generated}/creating-a-note/9/request-response.asciidoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/resources/application.properties b/samples/rest-notes-spring-data-rest/src/main/resources/application.properties new file mode 100644 index 00000000..8e06a828 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index 55fe34e8..e5902a61 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -39,7 +39,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index ec54ff5d..1657af9a 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -38,7 +38,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -66,33 +66,33 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()).build(); + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); } @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()))) - .andDo(document("index")); + .andExpect(status().isOk()) + .andExpect(jsonPath("_links.notes", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))); } @Test public void creatingANote() throws JsonProcessingException, Exception { String noteLocation = createNote(); - getNote(noteLocation); + MvcResult note = getNote(noteLocation); String tagLocation = createTag(); getTag(tagLocation); String taggedNoteLocation = createTaggedNote(tagLocation); - getTaggedNote(taggedNoteLocation); - getTags(taggedNoteLocation); + MvcResult taggedNote = getNote(taggedNoteLocation); + getTags(getLink(taggedNote, "tags")); tagExistingNote(noteLocation, tagLocation); - getTaggedExistingNote(noteLocation); - getTagsForExistingNote(noteLocation); + getTags(getLink(note, "tags")); } String createNote() throws Exception { @@ -106,18 +106,17 @@ public class GettingStartedDocumentation { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-note")) .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()))) - .andDo(document("get-note")); + MvcResult getNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andReturn(); } String createTag() throws Exception, JsonProcessingException { @@ -128,19 +127,16 @@ public class GettingStartedDocumentation { .perform( post("/tags").contentType(MediaTypes.HAL_JSON).content( objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tag")) - .andReturn().getResponse().getHeader("Location"); + .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()) + this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andDo(document("get-tag")); + .andExpect(jsonPath("_links.notes", is(notNullValue()))); } String createTaggedNote(String tag) throws Exception { @@ -155,28 +151,14 @@ public class GettingStartedDocumentation { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tagged-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getTaggedNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + void getTags(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("get-tagged-note")); - } - - 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))) - .andDo(document("get-tags")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { @@ -186,29 +168,24 @@ public class GettingStartedDocumentation { this.mockMvc.perform( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( objectMapper.writeValueAsString(update))) - .andExpect(status().isNoContent()) - .andDo(document("tag-existing-note")); - + .andExpect(status().isNoContent()); } - void getTaggedExistingNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + MvcResult getTaggedExistingNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) .andExpect(status().isOk()) - .andDo(document("get-tagged-existing-note")); + .andReturn(); } - void getTagsForExistingNote(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "tags"); - this.mockMvc.perform(get(tagsLocation)) + void getTagsForExistingNote(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags-for-existing-note")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } - private String getLink(MvcResult result, String href) + private String getLink(MvcResult result, String rel) throws UnsupportedEncodingException { return JsonPath.parse(result.getResponse().getContentAsString()).read( - "_links.tags.href"); + "_links." + rel + ".href"); } } diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc index ce970ea9..8fccc65c 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/request.asciidoc[] +include::{generated}/index/1/request.asciidoc[] This request should yield the following response: -include::{generated}/index/response.asciidoc[] +include::{generated}/index/1/response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -58,26 +58,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}/index/response.asciidoc[] +include::{generated}/index/1/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}/create-note/request.asciidoc[] +include::{generated}/creating-a-note/1/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}/create-note/response.asciidoc[] +include::{generated}/creating-a-note/1/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}/get-note/request.asciidoc[] +include::{generated}/creating-a-note/2/request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/response.asciidoc[] Note the `note-tags` link which we'll make use of later. @@ -91,26 +91,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}/index/response.asciidoc[] +include::{generated}/index/1/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}/create-tag/request.asciidoc[] +include::{generated}/creating-a-note/3/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}/create-tag/response.asciidoc[] +include::{generated}/creating-a-note/3/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}/get-tag/request.asciidoc[] +include::{generated}/creating-a-note/4/request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/get-tag/response.asciidoc[] +include::{generated}/creating-a-note/4/response.asciidoc[] @@ -131,44 +131,44 @@ with it. Once again we execute a `POST` request, but this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/create-tagged-note/request.asciidoc[] +include::{generated}/creating-a-note/5/request.asciidoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/create-tagged-note/response.asciidoc[] +include::{generated}/creating-a-note/5/response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/get-tagged-note/request-response.asciidoc[] +include::{generated}/creating-a-note/6/request-response.asciidoc[] To see the note's tags, execute a `GET` request against the URI of the note's `note-tags` link: -include::{generated}/get-tags/request.asciidoc[] +include::{generated}/creating-a-note/7/request.asciidoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/get-tags/response.asciidoc[] +include::{generated}/creating-a-note/7/response.asciidoc[] [getting-started-tagging-a-note-existing] === Tagging an existing note An existing note can be tagged by executing a `PATCH` request against the note's URI with -a body that contains the array of tags to be associated with the note. We'll used the +a body that contains the array of tags to be associated with the note. We'll use the URI of the untagged note that we created earlier: -include::{generated}/tag-existing-note/request.asciidoc[] +include::{generated}/creating-a-note/8/request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/tag-existing-note/response.asciidoc[] +include::{generated}/creating-a-note/8/response.asciidoc[] When we first created this note, we noted the `note-tags` link included in its details: -include::{generated}/get-note/response.asciidoc[] +include::{generated}/creating-a-note/2/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}/get-tags-for-existing-note/request-response.asciidoc[] +include::{generated}/creating-a-note/9/request-response.asciidoc[] 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 d82451c8..bcb731bf 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 @@ -39,7 +39,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; 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 49892b52..5b95a35a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * 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. @@ -23,7 +23,6 @@ import static org.springframework.restdocs.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; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 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; @@ -39,7 +38,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -67,7 +66,9 @@ public class GettingStartedDocumentation { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfigurer()).build(); + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}/{step}/")) + .build(); } @Test @@ -75,25 +76,23 @@ public class GettingStartedDocumentation { this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("index")); + .andExpect(jsonPath("_links.tags", is(notNullValue()))); } @Test public void creatingANote() throws JsonProcessingException, Exception { String noteLocation = createNote(); - getNote(noteLocation); + MvcResult note = getNote(noteLocation); String tagLocation = createTag(); getTag(tagLocation); String taggedNoteLocation = createTaggedNote(tagLocation); - getTaggedNote(taggedNoteLocation); - getTags(taggedNoteLocation); + MvcResult taggedNote = getNote(taggedNoteLocation); + getTags(getLink(taggedNote, "note-tags")); tagExistingNote(noteLocation, tagLocation); - getTaggedExistingNote(noteLocation); - getTagsForExistingNote(noteLocation); + getTags(getLink(note, "note-tags")); } String createNote() throws Exception { @@ -107,17 +106,17 @@ public class GettingStartedDocumentation { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getNote(String noteLocation) throws Exception { - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + MvcResult getNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) .andExpect(jsonPath("title", is(notNullValue()))) .andExpect(jsonPath("body", is(notNullValue()))) .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDo(document("get-note")); + .andReturn(); } String createTag() throws Exception, JsonProcessingException { @@ -130,7 +129,6 @@ public class GettingStartedDocumentation { objectMapper.writeValueAsString(tag))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tag")) .andReturn().getResponse().getHeader("Location"); return tagLocation; } @@ -138,8 +136,7 @@ public class GettingStartedDocumentation { void getTag(String tagLocation) throws Exception { this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.tagged-notes", is(notNullValue()))) - .andDo(document("get-tag")); + .andExpect(jsonPath("_links.tagged-notes", is(notNullValue()))); } String createTaggedNote(String tag) throws Exception { @@ -154,28 +151,14 @@ public class GettingStartedDocumentation { objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andExpect(header().string("Location", notNullValue())) - .andDo(document("create-tagged-note")) .andReturn().getResponse().getHeader("Location"); return noteLocation; } - void getTaggedNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + void getTags(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDo(document("get-tagged-note")); - } - - void getTags(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "note-tags"); - this.mockMvc.perform(get(tagsLocation)) - .andExpect(status().isOk()) - .andDo(print()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { @@ -185,24 +168,19 @@ public class GettingStartedDocumentation { this.mockMvc.perform( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( objectMapper.writeValueAsString(update))) - .andExpect(status().isNoContent()) - .andDo(document("tag-existing-note")); - + .andExpect(status().isNoContent()); } - void getTaggedExistingNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)) + MvcResult getTaggedExistingNote(String noteLocation) throws Exception { + return this.mockMvc.perform(get(noteLocation)) .andExpect(status().isOk()) - .andDo(document("get-tagged-existing-note")); + .andReturn(); } - void getTagsForExistingNote(String taggedNoteLocation) throws Exception { - String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) - .andReturn(), "note-tags"); - this.mockMvc.perform(get(tagsLocation)) + void getTagsForExistingNote(String noteTagsLocation) throws Exception { + this.mockMvc.perform(get(noteTagsLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))) - .andDo(document("get-tags-for-existing-note")); + .andExpect(jsonPath("_embedded.tags", hasSize(1))); } private String getLink(MvcResult result, String rel) diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java similarity index 93% rename from spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index f644cbbd..44f80511 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.config; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.request.RequestPostProcessor; @@ -100,6 +100,11 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { @Override public MockHttpServletRequest postProcessRequest( MockHttpServletRequest request) { + RestDocumentationContext currentContext = RestDocumentationContext + .currentContext(); + if (currentContext != null) { + currentContext.getAndIncrementStepCount(); + } request.setScheme(RestDocumentationConfigurer.this.scheme); request.setRemotePort(RestDocumentationConfigurer.this.port); request.setServerPort(RestDocumentationConfigurer.this.port); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java new file mode 100644 index 00000000..8d409952 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationContext.java @@ -0,0 +1,88 @@ +/* + * 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 org.springframework.restdocs.config; + +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@code RestDocumentationContext} encapsulates the context in which the documentation of + * a RESTful API is being performed. + * + * @author Andy Wilkinson + */ +public class RestDocumentationContext { + + private static final ThreadLocal CONTEXTS = new InheritableThreadLocal(); + + private final AtomicInteger stepCount = new AtomicInteger(0); + + private final Method testMethod; + + private RestDocumentationContext() { + this(null); + } + + private RestDocumentationContext(Method testMethod) { + this.testMethod = testMethod; + } + + /** + * Returns the test {@link Method method} that is currently executing + * + * @return The test method + */ + public Method getTestMethod() { + return this.testMethod; + } + + /** + * Gets and then increments the current step count + * + * @return The step count prior to it being incremented + */ + int getAndIncrementStepCount() { + return this.stepCount.getAndIncrement(); + } + + /** + * Gets the current step count + * + * @return The current step count + */ + public int getStepCount() { + return this.stepCount.get(); + } + + static void establishContext(Method testMethod) { + CONTEXTS.set(new RestDocumentationContext(testMethod)); + } + + static void clearContext() { + CONTEXTS.set(null); + } + + /** + * Returns the current context, never {@code null}. + * + * @return The current context + */ + public static RestDocumentationContext currentContext() { + return CONTEXTS.get(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java new file mode 100644 index 00000000..b685699b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationTestExecutionListener.java @@ -0,0 +1,40 @@ +/* + * 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 org.springframework.restdocs.config; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * A {@link TestExecutionListener} that sets up and tears down the Spring REST Docs + * context for each test method + * + * @author Andy Wilkinson + */ +public class RestDocumentationTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + RestDocumentationContext.establishContext(testContext.getTestMethod()); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + RestDocumentationContext.clearContext(); + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java new file mode 100644 index 00000000..86b9face --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/OutputFileResolver.java @@ -0,0 +1,101 @@ +/* + * 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 org.springframework.restdocs.snippet; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.restdocs.config.RestDocumentationContext; + +/** + * {@code OutputFileResolver} resolves an absolute output file based on the current + * configuration and context. + * + * @author Andy Wilkinson + */ +class OutputFileResolver { + + private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([A-Z])"); + + File resolve(String outputDirectory, String fileName) { + Map replacements = createReplacements(); + String path = outputDirectory; + for (Entry replacement : replacements.entrySet()) { + while (path.contains(replacement.getKey())) { + if (replacement.getValue() == null) { + throw new IllegalStateException("No replacement is available for " + + replacement.getKey()); + } + else { + path = path.replace(replacement.getKey(), replacement.getValue()); + } + } + } + + File outputFile = new File(path, fileName); + if (!outputFile.isAbsolute()) { + outputFile = makeRelativeToConfiguredOutputDir(outputFile); + } + return outputFile; + } + + private Map createReplacements() { + RestDocumentationContext context = RestDocumentationContext.currentContext(); + + Map replacements = new HashMap(); + replacements.put("{methodName}", context == null ? null : context.getTestMethod() + .getName()); + replacements.put("{method-name}", context == null ? null + : camelCaseToDash(context.getTestMethod().getName())); + replacements.put("{method_name}", context == null ? null + : camelCaseToUnderscore(context.getTestMethod().getName())); + replacements.put("{step}", + context == null ? null : Integer.toString(context.getStepCount())); + + return replacements; + } + + private String camelCaseToDash(String string) { + return camelCaseToSeparator(string, "-"); + } + + private String camelCaseToUnderscore(String string) { + return camelCaseToSeparator(string, "_"); + } + + private String camelCaseToSeparator(String string, String separator) { + Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, separator + matcher.group(1).toLowerCase()); + } + matcher.appendTail(result); + return result.toString(); + } + + private File makeRelativeToConfiguredOutputDir(File outputFile) { + File configuredOutputDir = new DocumentationProperties().getOutputDir(); + if (configuredOutputDir != null) { + return new File(configuredOutputDir, outputFile.getPath()); + } + return null; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 4ae21075..5df71176 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -56,10 +56,8 @@ public abstract class SnippetWritingResultHandler implements ResultHandler { } private Writer createWriter() throws IOException { - File outputFile = new File(this.outputDir, this.fileName + ".asciidoc"); - if (!outputFile.isAbsolute()) { - outputFile = makeRelativeToConfiguredOutputDir(outputFile); - } + File outputFile = new OutputFileResolver().resolve(this.outputDir, this.fileName + + ".asciidoc"); if (outputFile != null) { File parent = outputFile.getParentFile(); @@ -69,15 +67,9 @@ public abstract class SnippetWritingResultHandler implements ResultHandler { } return new FileWriter(outputFile); } - - return new OutputStreamWriter(System.out); - } - - private File makeRelativeToConfiguredOutputDir(File outputFile) { - File configuredOutputDir = new DocumentationProperties().getOutputDir(); - if (configuredOutputDir != null) { - return new File(configuredOutputDir, outputFile.getPath()); + else { + return new OutputStreamWriter(System.out); } - return null; } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/META-INF/spring.factories b/spring-restdocs/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..64519d78 --- /dev/null +++ b/spring-restdocs/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.test.context.TestExecutionListener=org.springframework.restdocs.config.RestDocumentationTestExecutionListener \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java new file mode 100644 index 00000000..7c485b44 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -0,0 +1,141 @@ +package org.springframework.restdocs; + +import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationIntegrationTests.TestConfiguration; +import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.test.context.ContextConfiguration; +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.setup.MockMvcBuilders; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * Integration tests for Spring REST Docs + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = TestConfiguration.class) +public class RestDocumentationIntegrationTests { + + @Autowired + private WebApplicationContext context; + + @Before + public void setOutputDirSystemProperty() { + System.setProperty("org.springframework.restdocs.outputDir", + "build/generated-snippets"); + } + + @Before + public void deleteSnippets() { + FileSystemUtils.deleteRecursively(new File("build/generated-snippets")); + } + + @After + public void clearOutputDirSystemProperty() { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + + @Test + public void basicSnippetGeneration() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("basic")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), + "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + } + + @Test + public void parameterizedOutputDirectory() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("{method-name}")); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/parameterized-output-directory"), + "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + } + + @Test + public void multiStep() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()) + .alwaysDo(document("{method-name}-{step}")).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-1/"), "request.asciidoc", + "response.asciidoc", "request-response.asciidoc"); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-2/"), "request.asciidoc", + "response.asciidoc", "request-response.asciidoc"); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( + status().isOk()); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-3/"), "request.asciidoc", + "response.asciidoc", "request-response.asciidoc"); + + } + + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { + for (String snippet : snippets) { + assertTrue(new File(directory, snippet).isFile()); + } + } + + @Configuration + @EnableWebMvc + static class TestConfiguration extends WebMvcConfigurerAdapter { + + @Bean + public TestController testController() { + return new TestController(); + } + + } + + @RestController + static class TestController { + + @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public Map foo() { + Map response = new HashMap(); + response.put("a", "alpha"); + return response; + } + + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java similarity index 95% rename from spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java rename to spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0b8d7e56..a4a93828 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.config; import static org.junit.Assert.assertEquals; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentationConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; /** diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java new file mode 100644 index 00000000..75755284 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/OutputFileResolverTests.java @@ -0,0 +1,155 @@ +/* + * 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 org.springframework.restdocs.snippet; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.restdocs.config.RestDocumentationTestExecutionListener; +import org.springframework.test.context.TestContext; + +/** + * Tests for {@link OutputFileResolver}. + * + * @author Andy Wilkinson + */ +public class OutputFileResolverTests { + + private final OutputFileResolver resolver = new OutputFileResolver(); + + @Test + public void noConfiguredOutputDirectoryAndRelativeInput() { + assertThat(this.resolver.resolve("foo", "bar.txt"), is(nullValue())); + } + + @Test + public void absoluteInput() { + String absolutePath = new File("foo").getAbsolutePath(); + assertThat(this.resolver.resolve(absolutePath, "bar.txt"), is(new File( + absolutePath, "bar.txt"))); + } + + @Test + public void configuredOutputAndRelativeInput() { + String outputDir = new File("foo").getAbsolutePath(); + System.setProperty("org.springframework.restdocs.outputDir", outputDir); + try { + assertThat(this.resolver.resolve("bar", "baz.txt"), is(new File(outputDir, + "bar/baz.txt"))); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + + @Test + public void configuredOutputAndAbsoluteInput() { + String outputDir = new File("foo").getAbsolutePath(); + String absolutePath = new File("bar").getAbsolutePath(); + System.setProperty("org.springframework.restdocs.outputDir", outputDir); + try { + assertThat(this.resolver.resolve(absolutePath, "baz.txt"), is(new File( + absolutePath, "baz.txt"))); + } + finally { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + } + + @Test(expected = IllegalStateException.class) + public void placeholderWithoutAReplacement() { + this.resolver.resolve("{method-name}", "foo.txt"); + } + + @Test + public void dashSeparatedMethodName() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("dashSeparatedMethodName"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat(this.resolver.resolve(new File("{method-name}").getAbsolutePath(), + "foo.txt"), + is(new File(new File("dash-separated-method-name").getAbsolutePath(), + "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } + + @Test + public void underscoreSeparatedMethodName() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("underscoreSeparatedMethodName"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat( + this.resolver.resolve(new File("{method_name}").getAbsolutePath(), + "foo.txt"), + is(new File(new File("underscore_separated_method_name") + .getAbsolutePath(), "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } + + @Test + public void camelCaseMethodName() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("camelCaseMethodName"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat(this.resolver.resolve(new File("{methodName}").getAbsolutePath(), + "foo.txt"), + is(new File(new File("camelCaseMethodName").getAbsolutePath(), + "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } + + @Test + public void stepCount() throws Exception { + RestDocumentationTestExecutionListener listener = new RestDocumentationTestExecutionListener(); + TestContext testContext = mock(TestContext.class); + Method method = getClass().getMethod("stepCount"); + when(testContext.getTestMethod()).thenReturn(method); + listener.beforeTestMethod(testContext); + try { + assertThat(this.resolver.resolve(new File("{step}").getAbsolutePath(), + "foo.txt"), is(new File(new File("0").getAbsolutePath(), "foo.txt"))); + } + finally { + listener.afterTestMethod(testContext); + } + } +}