-
Notifications
You must be signed in to change notification settings - Fork 6
Spring Rest Docs 적용기
*본 문서는 Rest Docs 가 무엇인지가 아니라, 프로젝트에서의 Rest Docs 사용법을 설명합니다.
본 문서는 아래와 같은 목차를 갖습니다.
- Rest Docs 간단 설명
- 프로젝트에 Rest Docs 적용 방법 설명
a. build.gradle에 Rest Docs 관련 의존성을 추가한다.
b. test/controller 패키지 내부에 RestDocsSupport.class, RestDocsConfiguration.class 파일을 생성한다.
c. 컨트롤러 테스트 코드를 작성한다.
d. 테스트 코드를 실행한 후,build/generated-snippets
하위에.adoc
문서들이 추가된 것을 확인한다.
e.src/docs/asciidocs/pickpick-api.adoc
문서를 생성하고operation::...(생략)
등 api docs를 작성한다.
f..adoc
을 작성 완료하면 build 하여 문서를 생성한다.
-
pickpick-api.adoc
문서의 항목 중operation::(생략)[snippets=(....)]
snippets에 작성해야할 항목에 대한 설명
a. HttpRequest, HttpResponse
b. PathParameters(PathVariable)
c. RequestParameters(RequestParam)
d. RequestFields(RequestBody)
e. ResponseFields(ResponseBody)
f. RequestHeaders
- 테스트 코드 기반으로 API 문서를 생성
- 테스트 코드가 실패하면 문서가 만들어지지 않기 때문에 테스트 코드로 검증된 문서를 보장할 수 있다.
build.gradle에 Rest Docs 관련 의존성을 추가합니다.
plugins {
**(생략)**
// asciidoctor 플러그인 적용(gradle7 버전 이상부터는 jvm 사용)
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
group = 'com.pickpick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
asciidoctorExtensions // dependencies 에서 적용한거 추가
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
repositories {
mavenCentral()
}
dependencies {
**(생략)**
// asciidoctor extension 추가
// 최종 .adoc 파일에서 operation을 사용하여 snippet 조각들을 연동할 수 있음
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
// mockMvc 를 사용하여 restdocs 하도록
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
ext {
//아래에서 사용할 변수 선언
snippetsDir = file('build/generated-snippets')
}
test {
// test의 output의 결과를 위에서 선언한 변수(build/generated-snippets)에 추가
outputs.dir snippetsDir
useJUnitPlatform()
}
// asciidoctor 설정
asciidoctor {
configurations 'asciidoctorExtensions'
inputs.dir snippetsDir
dependsOn test
}
// asciidoctor 가 실행될 때, 하단 경로 하위의 파일 삭제
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
// bootJar 시 실행
bootJar {
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'static/docs'
}
}
// from의 파일을 into로 카피
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
// build시 copydocument 실행
build {
dependsOn copyDocument
}
tasks.named('test') {
useJUnitPlatform()
}
/test/java/com.pickpick/controller/RestDocsConfiguration.class
@TestConfiguration
public class RestDocsConfiguration {
@Bean
public RestDocumentationResultHandler write() {
return MockMvcRestDocumentation.document(
"{class-name}/{method-name}",
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(Preprocessors.prettyPrint())
);
}
}
/test/java/com.pickpick/controller/RestDocsTestSupport.class
package com.pickpick.controller;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ExtendWith(RestDocumentationExtension.class)
@Import(RestDocsConfiguration.class)
class RestDocsTestSupport {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected RestDocumentationResultHandler restDocs;
@Autowired
private ResourceLoader resourceLoader;
@Autowired
protected ObjectMapper objectMapper;
@BeforeEach
void setUp(
final WebApplicationContext context,
final RestDocumentationContextProvider provider
) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.alwaysDo(MockMvcResultHandlers.print())
.alwaysDo(restDocs)
.build();
}
protected static String readJson(final String path) throws IOException {
return new String(Files.readAllBytes(Paths.get("src/test/resources", path)));
}
}
ㄴ> 이 클래스는 앞으로 rest docs에서 사용할 controller test에서 상속받아서 사용할 것이다.
@Sql({"/truncate.sql", "/channel.sql", "/channel-subscription.sql"})
class ChannelControllerTest extends RestDocsTestSupport {
@DisplayName("구독 여부를 포함하여 채널을 조회한다.")
@Test
void findAll() throws Exception {
mockMvc.perform(RestDocumentationRequestBuilders
.get("/api/channels")
.header(HttpHeaders.AUTHORIZATION, "Bearer 2"))
.andExpect(status().isOk())
.andDo(restDocs.document(
**requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("유저 식별 토큰(Bearer)")
),
responseFields(
fieldWithPath("channels.[].id").type(JsonFieldType.NUMBER).description("채널 아이디"),
fieldWithPath("channels.[].name").type(JsonFieldType.STRING).description("채널 이름"),
fieldWithPath("channels.[].isSubscribed").type(JsonFieldType.BOOLEAN).description("채널 구독 여부")
)**
));
}
}
.andDo() 내부 함수에서 requestHeaders, responseFields 등으로 docs에 대한 설정을 해준다.
위의 테스트코드를 실행하면 backend/build/generated-snippets/{컨트롤러_테스트_이름}/{테스트_함수_이름}
에 .adoc
파일이 생긴다.
src/docs/asciidoc/{파일이름}.adoc
을 생성한다.
= PickPick(a.k.a 줍줍) API Document
pickpick team
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[introduction]]
== 소개
줍줍 API 문서 입니다.
[[introduction]]
== 서비스환경
해당 API서비스의 서버 도메인 환경은 다음과 같습니다.
== Domain
|===
| 환경 | URI
| 개발서버
| `????`
| 운영서버
| `????`
|===
= 채널
== 채널 API
=== 전체 채널 조회 API
operation::channel-controller-test/find-all[snippets='http-request,request-headers,http-response,response-fields']
.adoc 파일 내부에 operation::channel-controller-test/find-all[snippets='http-request,request-headers,http-response,response-fields']
을 작성해야 docs 문서에서 확인할 수 있다.
operation::{컨트롤러-테스트-이름}/{테스트-함수-이름}[snippets={종류1},{종류2}]
형식으로 작성한다.
문서를 작성완료하면 build하고 서버를 띄운다. http://localhost:8080/docs/api.html 로 접속하면 만들어진 rest docs를 확인할 수 있다.
컨트롤러에 테스트를 작성하고 테스트를 실행하면 build/generated-snippets에 위 사진과 같이 .adoc
파일이 생긴다. 이 파일 중 api 문서에 어떤 .adoc을 포함해야할까?
operation::channel-controller-test/find-all[snippets='http-request,http-response']
로 넣어준다면 api docs에서 위 내용을 확인할 수 있다.
기본적인 http-request와 http-response는 파일 내부에 존재하도록 하는 것이 좋겠다.
=> 이 때 주의사항: mockMvcBuilder가 아니라 RestDocumentationRequestBuilders를 사용하기
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
//생략
.andDo(restDocs.document(
pathParameters(
parameterWithName("id").description("채널 아이디")
)
)
);
컨트롤러 테스트 코드에서 .andDo()
내부에 pathParameters를 정의해준 후 api.adoc 파일을 수정한 후 실행하면 api docs에서 아래 이미지를 확인할 수 있다.
operation::channel-controller-test/find-all[snippets='path-parameters']
.andDo(restDocs.document(
requestParameters(
parameterWithName("keyword").optional().description("검색할 키워드"),
parameterWithName("date").optional().description("검색 기준 날짜"),
parameterWithName("channelIds").optional().description("검색할 채널 아이디(복수 가능)"),
parameterWithName("needPastMessage").optional().description("불러올 메시지가 더 존재하는지"),
parameterWithName("messageId").optional().description("메시지 아이디"),
parameterWithName("messageCount").optional().description("한 번에 불러올 메시지 개수(default:20)")
)
)
);
컨트롤러 테스트 코드에서 .andDo()
내부에 requestParameters를 정의해준 후 api.adoc 파일을 수정한 후 실행하면 api docs에서 아래 이미지를 확인할 수 있다.
operation::channel-controller-test/find-all[snippets='request-parameters']
.andDo(restDocs.document(
requestFields(
fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("채널 아이디"),
fieldWithPath("[].order").type(JsonFieldType.NUMBER).description("구독 채널 순서")
)
)
);
컨트롤러 테스트 코드에서 .andDo()
내부에 requestFields를 정의해준 후 api.adoc 파일을 수정한 후 실행하면 api docs에서 아래 이미지를 확인할 수 있다.
operation::channel-controller-test/find-all[snippets='request-fields']
*실제 RequestBody 예시
.andDo(restDocs.document(
responseFields(
fieldWithPath("channels.[].id").type(JsonFieldType.NUMBER).description("채널 아이디"),
fieldWithPath("channels.[].name").type(JsonFieldType.STRING).description("채널 이름"),
fieldWithPath("channels.[].isSubscribed").type(JsonFieldType.BOOLEAN).description("채널 구독 여부")
)
)
);
컨트롤러 테스트 코드에서 .andDo()
내부에 responseFields를 정의해준 후 api.adoc 파일을 수정한 후 실행하면 api docs에서 아래 이미지를 확인할 수 있다.
operation::channel-controller-test/find-all[snippets='response-fields']
*실제 ResponseBody 예시
.andDo(restDocs.document(
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("유저 식별 토큰(Bearer)")
)
)
);
컨트롤러 테스트 코드에서 .andDo()
내부에 requestHeaders를 정의해준 후 api.adoc 파일을 수정한 후 실행하면 api docs에서 아래 이미지를 확인할 수 있다.
operation::channel-controller-test/find-all[snippets='request-headers']