[사용방법 참고]
[Spring.io Guide] Creating API Documentation with Restdocs
[ Spring Docs ] Spring RestDocs, Setting up your tests
사용 예시 참고
우아한 형제들 Spring RestDocs에 날개를 — adoc 파일의 table of content, 파일 폰트, highlight 설정 등의 내용
Spring RestDocs 적용 및 최적화 — print 공통 처리, generated snippet의 네이밍 설정 등 Bean으로 설정하여 계속 반복적 작업 줄여주는 내용 포함되어 있음
의존성 추가이용방법1. 테스트 코드 작성 with document( ) 2. 생성된 adoc파일들을 이용하여 index.adoc을 작성asciidoc 문법목록 추가, 속성 추가3. 해당 index.adoc을 html 파일로 변경요청, 응답 명세 작성 시 사용하는 메서드요청과 응답 커스터마이징모든 테스트에 대해 동일한 preprocessor 를 적용하기REST DOCS 생성 파일 jar에 포함시키기
의존성 추가
<dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency>
plugins { id "org.asciidoctor.jvm.convert" version "3.3.2" } configurations { asciidoctorExt } dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' } ext { snippetsDir = file('build/generated-snippets') } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir configurations 'asciidoctorExt' dependsOn test }
이용방법
1. 테스트 코드 작성 with document( )
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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.status; @AutoConfigureMockMvc @AutoConfigureRestDocs @SpringBootTest class OrderControllerTest { @Test void createOrderTest() throws Exception { mvc.perform(post("/orders") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(orderDtoData))) .andExpect(status().isOk()) .andDo(print()) .andDo(document("order-save", requestFields( fieldWithPath("uuid").type(JsonFieldType.STRING).description("UUID"), fieldWithPath("orderDatetime").type(JsonFieldType.STRING).description("orderDatetime"), fieldWithPath("orderStatus").type(JsonFieldType.STRING).description("orderStatus"), fieldWithPath("memo").type(JsonFieldType.STRING).description("memo"), fieldWithPath("memberDto").type(JsonFieldType.OBJECT).description("memberDto"), fieldWithPath("memberDto.id").type(JsonFieldType.NUMBER).description("memberDto.id"), fieldWithPath("memberDto.name").type(JsonFieldType.STRING).description("memberDto.name"), fieldWithPath("memberDto.nickName").type(JsonFieldType.STRING).description("memberDto.nickName"), fieldWithPath("memberDto.age").type(JsonFieldType.NUMBER).description("memberDto.age"), fieldWithPath("memberDto.address").type(JsonFieldType.STRING).description("memberDto.address"), fieldWithPath("memberDto.description").type(JsonFieldType.STRING).description("memberDto.description"), fieldWithPath("orderItemDtos").type(JsonFieldType.ARRAY).description("orderItemDtos"), fieldWithPath("orderItemDtos[].id").type(JsonFieldType.NUMBER).description("orderItemDtos[].id"), fieldWithPath("orderItemDtos[].quantity").type(JsonFieldType.NUMBER).description("orderItemDtos[].quantity"), fieldWithPath("orderItemDtos[].price").type(JsonFieldType.NUMBER).description("orderItemDtos[].price"), fieldWithPath("orderItemDtos[].itemDto").type(JsonFieldType.OBJECT).description("orderItemDtos[].itemDto"), fieldWithPath("orderItemDtos[].itemDto.id").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.id"), fieldWithPath("orderItemDtos[].itemDto.type").type(JsonFieldType.STRING).description("orderItemDtos[].itemDto.type"), fieldWithPath("orderItemDtos[].itemDto.price").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.price"), fieldWithPath("orderItemDtos[].itemDto.stockQuantity").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.stockQuantity"), fieldWithPath("orderItemDtos[].itemDto.power").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.power"), fieldWithPath("orderItemDtos[].itemDto.chef").type(JsonFieldType.NULL).description("orderItemDtos[].itemDto.chef"), fieldWithPath("orderItemDtos[].itemDto.width").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.width"), fieldWithPath("orderItemDtos[].itemDto.height").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.height") ), responseFields( fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), fieldWithPath("data").type(JsonFieldType.STRING).description("데이터"), fieldWithPath("serverDateTime").type(JsonFieldType.STRING).description("서버시간") ))); } }
- 테스트 클래스에 @AutoConfigureRestDocs 적용
- 이렇게 해서 테스트 할 시, target/generated-snippets/{order-save}(document 메서드 안에 명시하는 이름)에 해당하는 asciidocs 파일들이 생성됨
- 해당 ascii docs들을 이용하여 index.adoc을 작성함
Parameterized Output Directories

2. 생성된 adoc파일들을 이용하여 index.adoc을 작성
![빌드툴 별로 asciidoc 소스파일들과 plugin을 사용해서 생성한 html의 위치가 다름 [RESTDOCS 문서 참고]](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7630cee2-1e00-4480-a1b4-f38701753888%2FUntitled.png?table=block&id=fb6b888e-da82-49bf-8f57-8db21d5ab574&cache=v2)
:hardbreaks: ifndef::snippets[] :snippets: ../../../target/generated-snippets endif::[] == 주문 === 주문 생성 === /orders/{uuid} .Request include::{snippets}/order-save/http-request.adoc[] include::{snippets}/order-save/request-fields.adoc[] operation::user-controller-test/로그인_성공[snippets='request-fields,response-fields'] .Response include::{snippets}/order-save/http-response.adoc[] include::{snippets}/order-save/response-fields.adoc[]
asciidoc 문법
섹션 제목
= Document Title (Level 0) == Level 1 Section === Level 2 Section ==== Level 3 Section ===== Level 4 Section ====== Level 5 Section
[[]]
: 이건 그냥 코멘트 용도로 사용하는 듯함
‘’’
: 라인 생성
목록 추가, 속성 추가
[[REALWORLD]] = Realworld :doctype: book :icons: font :source-highlighter: highlightjs // 문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용 :toc: left. // table of contents. 왼쪽에 목차 위치 :toclevels: 2 // 몇 단계 레벨까지 나타낼지 :sectlinks: :docinfo: shared-head include::API/user-api.adoc[]
3. 해당 index.adoc을 html 파일로 변경

- 위 링크 참고하면 plugin 설정하는 방법이 있는데 plugin을 이용하면 알아서 html 파일 생성해줌
- 조금 아래에 살펴보면 Packaging the documentation도 있음. 배포할 때 static content로 포함시켜서 배포하기
요청, 응답 명세 작성 시 사용하는 메서드
[ Spring Docs] REST DOCS — Documenting your API
// PathParameter에 대한 명세( pathParameters를 쓸 때는 RestDocumentationRequestBuilders // 를 이용하여 호출해야함! // org.springframework.restdocs.request.RequestDocumentation mockMvc.perform( RestDocumentationRequestBuilders.delete(ENDPOINT_URL_PREFIX + "{userCategoryId}", userCategoryId)) .andExpect(status().isNoContent()) .andDo( MockMvcRestDocumentation.document("aa", pathParameters(parameterWithName("userCategoryId").description("카테고리 아이디")) ) ); // requestBody, responseBody mockMvc.perform(patch(ENDPOINT_URL_PREFIX + "/{userCategoryId}", userCategoryId) .content(objectMapper.writeValueAsString(body)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo( restDocs.document( PayloadDocumentation.requestFields( fieldWithPath("name").type(JsonFieldType.STRING).description("업데이트할 카테고리 이름") ), responseFields( fieldWithPath("name").type(JsonFieldType.STRING).description("업데이트 된 카테고리 이름") ) ) ); // QueryParameter 명세 mockMvc.perform(RestDocumentationRequestBuilders.get(ENDPOINT_URL_PREFIX).queryParam("kind", categoryType)) .andExpect(status().isOk()) .andDo( restDocs.document( RequestDocumentation.requestParameters(( RequestDocumentation.parameterWithName("kind").description("카테고리 종류") ), responseFields( fieldWithPath("categories[].id").type(JsonFieldType.NUMBER).description("카테고리 아이디"), fieldWithPath("categories[].name").type(JsonFieldType.STRING).description("카테고리 이름"), fieldWithPath("categories[].categoryType").type(JsonFieldType.STRING).description("카테고리 종류") ) ) ); // binary payload에 대한 API 명세는 지원하지 않음
RestDocs 사용 클래스
RequestDocumentation
: queryParam, pathVariable 에 대해서 사용하는 클래스
PayloadDocumentation
HeaderDocumentation
요청과 응답 커스터마이징
[ Spring Docs ] REST DOCS — Customizing requests and responses
- request와 response에 대해서
preprocesor
를 이용하여 문서화 하기 전에 변형을 가함
모든 테스트에 대해 동일한 preprocessor 를 적용하기
@TestConfiguration public class RestDocsConfig { @Bean public RestDocumentationResultHandler resultHandler() { return MockMvcRestDocumentation.document( "{class-name}/{method-name}", Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) ); } }
@SpringBootTest @Import(RestDocsConfig.class) @ActiveProfiles("test") @ExtendWith(RestDocumentationExtension.class) public abstract class AbstractControllerTest { @Autowired protected RestDocumentationResultHandler restDocs; protected MockMvc mvc; protected final ObjectMapper objectMapper = new ObjectMapper(); protected final ResponseFieldsSnippet commonResponse = PayloadDocumentation.responseFields( PayloadDocumentation.fieldWithPath("code").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), PayloadDocumentation.fieldWithPath("message").type(JsonFieldType.STRING).description("상태 메시지"), PayloadDocumentation.fieldWithPath("data[]").type(JsonFieldType.ARRAY).description("응답 본문")); @BeforeEach public void setup(RestDocumentationContextProvider provider, WebApplicationContext context) { this.mvc = MockMvcBuilders.webAppContextSetup(context) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) .apply(SecurityMockMvcConfigurers.springSecurity()) .alwaysDo(restDocs) .addFilter(new CharacterEncodingFilter("UTF-8", true)) .build(); }
[ MockMvcBuilders 사용시 유의점 ]
@AutoconfigureMockMvc @SpringBootTest
: 애플리케이션을 실행하지 않고, Spring이 HTTP request를 handle 하고 controller에 넘겨주는 것 까지만 테스트 하는 방법 : SecurityMockMvcConfigurers.springSecurity()
적용 안하면 SecurityFilter들이 제외됨MockMvcBuilders에서 alwaysDo(restDocs)
를 적용시켜두면, mockMvc로 호출만 해도, 아래 adoc파일이 자동으로 생성됨

REST DOCS 생성 파일 jar에 포함시키기
tasks.named('bootJar') { dependsOn asciidoctor from ("build/docs/asciidoc") { into "static/docs" } }