Spring REST Docs
Spring MVC Test๋ฅผ ์ด์ฉํด์ REST API ๋ฌธ์์ ์กฐ๊ฐ(snippets
)์ ์์ฑํ๋๋ฐ ๋์์ ์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๊ธฐ๋ณธ์ ์ผ๋ก Asciidoctor
๋ฅผ ์ฌ์ฉํ์ฌ htmlํ์ผ์ ์์ฑํ๊ณ , ์ํ๋ ๊ฒฝ์ฐ Markdown์ ์ฌ์ฉํ ์ ์๋ค.
API๋ฅผ ์ ์ํ๋๋ฐ ์ฌ์ฉํ๋ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ Swagger
๊ฐ ์๋ค.
Swagger์์ ์ฐจ์ด์
Swagger
๋ API๋ฅผ ํ
์คํธ
ํด๋ณผ ์ ์๋ ํ๋ฉด์ ์ ๊ณต
ํ๊ณ ์ค์ ์ฝ๋
์ ์ด๋
ธํ
์ด์
์ ์ถ๊ฐ
ํด์ผํ์ง๋ง Spring Rest Docs
๋ ํ
์คํธ์ฝ๋๋ฅผ ํตํด ์์ฑ๋๋ฏ๋ก ์ค์ ์ฝ๋์๋ ์ํฅ์ด ์๋ค.
์ฌ์ฉ๋ฒ
MockMvc, WebTestClient๋ฑ์ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ฉฐ test์ ์ฌ์ฉํ๋ mockMvc๋ฅผ ๋ง๋ค๋ documentationConfiguration์ ์ฌ์ฉํ์ฌ ์์ฑํ๊ณ MockMvc๋ก ํ ์คํธ ์ค andDo๋ฅผ ์ด์ฉํ์ฌ ์์ฑ
Spring Boot์์๋ ๊ฐ๋จํ
@AutoConfigureRestDocs
๋ง ๋ถ์ฌ์ฃผ๋ฉด ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
Test๋ด์ฉ์ด ๋ฐ๋๊ฑฐ๋ ๋ค์ ์คํํ ๋๋ง๋ค Overrideํ์ฌ ๋ฌธ์๊ฐ ๋ค์ ์์ฑ๋๋ค.
๋ฉ์๋
andDo(document(โdoc-nameโ, snippets))
snippets
links()
requestParameters() + parameterWithName()
pathParameters() + parametersWithName()
requestParts() + partWithname()
requestPartBody()
requestPartFields()
requestHeaders() + headerWithName()
requestFields() + fieldWithPath()
responseHeaders() + headerWithName()
responseFields() + fieldWithPath()
...
this.mockMvc.perform(post("/api/events") .contentType(MediaType.APPLICATION_JSON) .accept(MediaTypes.HAL_JSON) .content(this.objectMapper.writeValueAsString(event))) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath("id").exists()) .andExpect(header().exists(HttpHeaders.LOCATION)) .andExpect(header().string(HttpHeaders.CONTENT_TYPE,"application/hal+json;charset=UTF-8")) .andExpect(jsonPath("free").value(false)) .andExpect(jsonPath("offline").value(true)) .andExpect(jsonPath("eventStatus").value(EventStatus.DRAFT.name())) .andExpect(jsonPath("_links.self").exists()) .andExpect(jsonPath("_links.query-events").exists()) .andExpect(jsonPath("_links.update-event").exists()) .andDo(document("create-event", links( linkWithRel("self").description("link to self"), linkWithRel("query-events").description("link to query-events"), linkWithRel("update-event").description("link to update-event") ), requestHeaders( headerWithName(HttpHeaders.ACCEPT).description("accept header"), headerWithName(HttpHeaders.CONTENT_TYPE).description("content type header") ), requestFields( fieldWithPath("name").description("Name of Event"), fieldWithPath("description").description("Description of Event"), fieldWithPath("beginEnrollmentDateTime").description("๋ฑ๋ก์์๊ธฐ๊ฐ of Event"), fieldWithPath("closeEnrollmentDateTime").description("๋ฑ๋ก๋ง๊ฐ๊ธฐ๊ฐ of Event"), fieldWithPath("beginEventDateTime").description("์์๊ธฐ๊ฐ of Event"), fieldWithPath("endEventDateTime").description("์ข ๋ฃ๊ธฐ๊ฐ of Event"), fieldWithPath("location").description("location of Event"), fieldWithPath("basePrice").description("BasePrice of Event"), fieldWithPath("maxPrice").description("MaxPrice of Event"), fieldWithPath("limitOfEnrollment").description("๋ฑ๋ก ์ ํ of Event") ) ,responseHeaders( headerWithName(HttpHeaders.LOCATION).description("location header"), headerWithName(HttpHeaders.CONTENT_TYPE).description("content type header") ), responseFields( fieldWithPath("id").description("identifier of Event"), fieldWithPath("name").description("Name of Event"), fieldWithPath("description").description("Description of Event"), fieldWithPath("beginEnrollmentDateTime").description("๋ฑ๋ก์์๊ธฐ๊ฐ of Event"), fieldWithPath("closeEnrollmentDateTime").description("๋ฑ๋ก๋ง๊ฐ๊ธฐ๊ฐ of Event"), fieldWithPath("beginEventDateTime").description("์์๊ธฐ๊ฐ of Event"), fieldWithPath("endEventDateTime").description("์ข ๋ฃ๊ธฐ๊ฐ of Event"), fieldWithPath("location").description("location of Event"), fieldWithPath("basePrice").description("BasePrice of Event"), fieldWithPath("maxPrice").description("MaxPrice of Event"), fieldWithPath("limitOfEnrollment").description("๋ฑ๋ก ์ ํ of Event"), fieldWithPath("offline").description("์คํ๋ผ์ธ์ธ์ง of Event"), fieldWithPath("free").description("๋ฌด๋ฃ์ธ์ง of Event"), fieldWithPath("eventStatus").description("eventStatus of Event"), fieldWithPath("_links.self.href").description("link to self"), fieldWithPath("_links.query-events.href").description("link to query-events"), fieldWithPath("_links.update-event.href").description("link to update-event") ) ));
Relaxed* : ๋ชจ๋ ํ๋์ ๋ํด ๊ธฐ์ ํ์ง ์์๋ ๋ฌธ์ํ๋ฅผ ์์ผ์ฃผ๊ธฐ ์ํ prefix
๋ฌธ์ ์ผ๋ถ๋ถ๋ง ํ ์คํธ ํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ์ ํํ ๋ฌธ์ํ๊ฐ ๋ถ๊ฐ๋ฅ ํ๋ค.
Processor
preprocessRequest(prettyPrint())
preprocessResponse(prettyPrint())
configurer.operationPreprocessors().withRequestDefaults(prettyPrint()).withResponseDefaults(prettyPrint());
Customize
jsonํํ๋ฅผ ํ์ค๋ก ํฌ๋งทํ
ํ์ง ์์ ์ฑ๋ก responseํ๊ธฐ ๋๋ฌธ์ ๋ณด๊ธฐ๊ฐ ๋งค์ฐ ๋ถํธํ๊ธฐ ๋๋ฌธ์ ๋ณด๊ธฐ ํธํ๊ฒ foramattingํ์ฌ ์ถ๋ ฅํ๊ฒ RestDocksMockMvcConfigurationCustomizer ๊ตฌํํ ๋น์ ๋ฑ๋ก(๋ด๋ถ์ prettyPrint ์ด์ฉ)ํ์ฌ ์ฌ์ฉํ๊ณ Testํด๋์ค์ @Import(RestDocsConfiguration.class)
์ ํตํด ์์ฑํ ์ปค์คํ
์ค์ ์ ์ ์ฉํด์ผ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
@TestConfiguration //Test์๋ง ์ฌ์ฉํ๋ configuration์ด๋ค.
public class RestDocsConfiguration {
@Bean
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer(){
return new RestDocsMockMvcConfigurationCustomizer() {
@Override
public void customize(MockMvcRestDocumentationConfigurer configurer) {
configurer.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint());
}
}
}
}
//replace๊ธฐ๋ฅ ์ด์ฉํ์ฌ ๋๋ค๋ก ๋ฐ๊พธ๋ฉด
@TestConfiguration
public class RestDocsConfiguration {
@Bean
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer(){
return configurer -> configurer.operationPreprocessors().withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint());
}
}
build
buildํ๊ธฐ ์ํด dependecny ์ถ๊ฐ
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
</dependencies>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.outputDirectory}/static/docs
</outputDirectory>
<resources>
<resource>
<directory>
${project.build.directory}/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
mainํจํค์ง ์๋ asciidoc ํด๋๋ฅผ ๋ง๋ค์ด
index.adoc
ํ์ผ ์์ฑmaven์ package๋ก build
๋น๋ํ๋ฉด
asciidoctor-maven-plugin
์ด asciidocํ์ผ์ html๋ก ๋ง๋ค์ด genreated-docs ์๋index.html
์ด ์์ฑ๋๋ค.maven-resources-plugin
์ ๋ฐ๋ผ build๋ generated-docs์ ์์ฑํ ํ์ผ์ build์ staticํ์ผ ์๋ ์์ฑํด์ค์ผ๋ก์จ spring bootํน์ฑ์ ์ด์ฉํด ์๋ฒ์คํ์ url๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
Reference
https://docs.spring.io/spring-restdocs/docs/2.0.5.RELEASE/reference/html5/
https://narusas.github.io/2018/03/21/Asciidoc-basic.html
Last updated