์คํ ํ๊ฒฝ
- Spring Boot
2.7.1
- Gradle
7.4.1
- Maven
3.8.5
๋น๋ ์์คํ ์ค์
Gradle
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'org.asciidoctor.jvm.convert' version '3.3.2' // (1)
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
snippetsDir = file('build/generated-snippets') // (2)
}
test {
outputs.dir snippetsDir // (3)
useJUnitPlatform()
}
asciidoctor {
inputs.dir snippetsDir // (4)
dependsOn test // (5)
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // (6)
}
- (1) ํด๋น ํ๋ฌ๊ทธ์ธ์ .adoc ํ์ผ ๋ณํ๊ณผ build/ ๋๋ ํ ๋ฆฌ์ ๋ณต์ฌํด์ฃผ๋ ํ๋ฌ๊ทธ์ธ์ ๋๋ค. Gradle ๋ฒ์ 7 ์ด์๋ถํฐ 'org.asciidoctor.jvm.convert' ๋ก ๋ณ๊ฒฝ๋์๋ค๊ณ ํฉ๋๋ค.
- (2) Snippets ์์ฑ์์น๋ฅผ ์ ์ญ ๋ณ์๋ก ํ ๋นํฉ๋๋ค.
- (3) test์ ์ถ๋ ฅ ๋๋ ํ ๋ฆฌ๋ฅผ Snippets ๋๋ ํ ๋ฆฌ๋ก ์ค์ ํฉ๋๋ค.
- (4) ์
๋ ฅ ๋๋ ํ ๋ฆฌ๋ฅผ Snippets ๋๋ ํ ๋ฆฌ๋ก ์ค์ ํฉ๋๋ค.
- (5) ํด๋น ์์ ์ test → asciidoctor ์์ผ๋ก ์คํ๋ฉ๋๋ค.
- (6) test-scope mockMvc์ ์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
Maven
<!-- (1) -->
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<!-- REST DOCS -->
<!-- asciidoctor ํ๋ฌ๊ทธ์ธ ์ถ๊ฐ -->
<!-- (2) -->
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.2</version>
<executions>
<execution>
<id>generate-docs</id>
<!-- (3) -->
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<!-- (4) -->
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
- (1) Test scope RestDocs MockMvc๋ฅผ ์ถ๊ฐํฉ๋๋ค.
- (2) asciidoctor ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํฉ๋๋ค.
- (3) prepare-package ์ต์ ์ ํจํค์ง๋ด์ API ๋ฌธ์๋ฅผ ํฌํจํ ์ ์๊ฒ ํฉ๋๋ค.
- (4) asciidoctor์ ์์กด์ฑ์ผ๋ก spring-restdocs-asciidoctor ์ถ๊ฐ .adoc ํ์ผ์ด target/generated-snippets ์๋์ ์์ฑ๋ Snippets ์ ๊ฐ๋ฆฌํค๋ ์ค์ ์ด ์ถ๊ฐ๋๋ค.
๋ฌธ์ ํจํค์ง (Packaging the Documentation)
Maven
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<!-- ์๋ต.. -->
</plugin>
<!-- (1) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!-- (2) -->
<outputDirectory>
${project.build.outputDirectory}/static/docs
</outputDirectory>
<resources>
<resource>
<directory>
${project.build.directory}/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
- (1) ๋ฌธ์๊ฐ ํจํค์ง๋ก ๋ณต์ฌ๋๊ธฐ ์ ์ ์์ฑ๋์ผ ํ๋ฏ๋ก ์์ Asciidoctor ํ๋ฌ๊ทธ์ธ ๋ค์ ์ ์ธํฉ๋๋ค.
- (2) generated-docs/index.html์ static/docs ์ ๋ณต์ฌ
Gradle
bootJar {
dependsOn asciidoctor // (1)
copy { // (2)
from "${asciidoctor.outputDir}"
into 'src/main/resources/static/docs'
}
}
- (1) ํด๋น ์์ ์ asciidoctor → bootJar ์์ผ๋ก ์คํ๋ฉ๋๋ค.
- (2) asciidoctor๊ฐ ์์ฑํด์ค build/docs/asciidoc์ ์๋ HTML ํ์ผ์ด src/main/resources/static/docs ๋๋ ํ ๋ฆฌ์ ์์ฑ๋ฉ๋๋ค. static ์๋์ ์๋ ํ์ผ๋ค์ ํฐ์บฃ์ด ์๋์ผ๋ก ํธ์คํ ์ ํด์ฃผ๊ธฐ ๋๋ฌธ์ http://localhost:8080/docs/index.html ๋ก ์ ๊ทผํ ์ ์์ต๋๋ค.
TestSupport ํด๋์ค ์์ฑ
๊ธฐ๋ณธ ๊ณตํต ์ค์ ๋ค์ ๋ฐ๋ก ํด๋์ค์ ๋นผ๋๋ก ํ๊ฒ ์ต๋๋ค.
์ฌ๊ธฐ์ ์ค์ ์ ๋๋ ํ ๋ฆฌ๊ฐ ํด๋์ค๋ช
/๋ฉ์๋๋ช
์ผ๋ก ํ์ฑ์ด ๋ฉ๋๋ค.
@ExtendWith(RestDocumentationExtension.class)
๋ฅผ ์ ์ฉํฉ๋๋ค.
• RestDocumentationExtension ์ Maven ์ ๊ฒฝ์ฐ “target/generated-snippets” Gradle ์ ๊ฒฝ์ฐ “build/generated-snippets” ๋ฅผ ์๋์ผ๋ก ์ถ๋ ฅ ๋๋ ํ ๋ฆฌ๋ก ์ค์ ๋์ด์๋ค.
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@ExtendWith(RestDocumentationExtension.class)
public class TestSupport {
protected MockMvc mockMvc;
@BeforeEach
void setUp(WebApplicationContext context,
RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.alwaysDo(print())
.alwaysDo(document("{class-name}/{method-name}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())
)
)
.build();
}
}
์ ์ค์ ์ด๋ ธํ ์ด์ ์ผ๋ก ํ๋ฐฉ์ ์ค์ ์ ์ฉํ๊ธฐ!
@AutoConfigureMockMvc // --> webAppContextSetup(webApplicationContext)
@AutoConfigureRestDocs // --> apply(MockMvcRestDocumentation.documentationConfigration(restDocumentationContextpProvider)
REFERENCE
get, post ํจํค์ง ๋ณ๊ฒฝ
๊ธฐ์กด ์ฌ์ฉ์ค์ด๋ MockMvcRequestBuilders
์์ RestDocumentationRequestBuilders
๋ก ๋ณ๊ฒฝ
//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
/*
๊ธฐ์กด MockMvcRequestBuilders ํจํค์ง์์ RestDocumentationRequestBuilders๋ก ๋ณ๊ฒฝ!
*/
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
์ ์ด์ ๋ชจ๋ ์ค๋น๊ฐ ๋๋ฌ์ต๋๋ค. ํ ์คํธ๋ฅผ ์์ฑํ์ฌ ๋ฌธ์๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
ํ ์คํธ ์์ฑ
REST Docs๋ฅผ ์์ฑํ๊ธฐ ์ํด์๋ ๊ตณ์ด @SpringBootTest
๋ฅผ ์งํํ ์ด์ ๊ฐ ์์ต๋๋ค.
์๊ฐ๋ ์ค๋ ๊ฑธ๋ฆฌ๊ณ ๋ชจ๋ ๋น๋ค์ด ์ฌ๋ผ๊ฐ๊ธฐ ๋๋ฌธ์ ์ ๊ฒฝ์จ์ค์ผ ํ ๋ถ๋ถ๋ค์ด ๋ง๊ธฐ ๋๋ฌธ์ ๋๋ค.
๊ทธ๋์ ๋๋ถ๋ถ WebMvcTest
ํ์ํ ์์กด์ฑ๋ค์ Mocking
ํ์ฌ ํ
์คํธ๋ฅผ ์งํํฉ๋๋ค.
์๋๋ controller ํ ์คํธ ์ฝ๋์ ๋๋ค.
@WebMvcTest(controller = UserTodoListApiController.class)
public class UserAccountApiDocsTest extends RestDocumentSupport {
@MockBean
private AccountService accountService;
@Test
@WithMockUser
void retrieve_My_Account() throws Exception {
// given
AccountDto accountDto = AccountDto.builder()
.id(5L)
.username("test")
.password("pass")
.email("test@email.com")
.nickname("nickname")
.roles("USER")
.todoList(null)
.build();
given(accountService.findMyAccount(anyString())).willReturn(accountDto);
// when & then
mockMvc.perform(get("/user/accounts/{username}", "john1234")
.header("Authorization", "testToken"))
.andDo(print())
.andDo(document("{class-name}/{method-name}",
requestHeaders( // (1)
headerWithName("Authorization").description("JWT ํ ํฐ ๊ฐ")
),
pathParameters( // (2)
parameterWithName("username").description("์ฌ์ฉ์ ์์ด๋")
),
responseFields( // (3)
fieldWithPath("id").description("idx"),
fieldWithPath("username").description("์์ด๋"),
fieldWithPath("password").description("ํจ์ค์๋"),
fieldWithPath("nickname").description("๋๋ค์"),
fieldWithPath("email").description("์ด๋ฉ์ผ"),
fieldWithPath("roles").description("๊ถํ"),
fieldWithPath("todoList").ignored()
)
)
)
.andExpect(status().isOk())
;
}
๋ฌธ์ํ๋ฅผ ํ๊ธฐ ์ํด์๋ ์์ฒญ๊ณผ ์๋ต์ ํฌํจ๋ ํค๋, ๋ฐ๋ ๋ฑ๋ฑ์ ํฌํจ๋ ๊ฐ๋ค์ ๋ช ์ธํด์ค์ผ ํฉ๋๋ค.
- (1) Request Header์ ๋ค์ด๊ฐ๋ ๊ฐ์ ๋ช
์ธํฉ๋๋ค.
- header๊ฐ์ด๊ธฐ ๋๋ฌธ์
headerWithName
์ผ๋ก ๋ช ์ธํฉ๋๋ค.
- header๊ฐ์ด๊ธฐ ๋๋ฌธ์
- (2) Path Parameter์ ๋ค์ด๊ฐ๋ ๊ฐ์ ๋ช
์ธํฉ๋๋ค.
- Parameter์ด๊ธฐ ๋๋ฌธ์
parameterWithName
์ผ๋ก ๋ช ์ธํฉ๋๋ค.
- Parameter์ด๊ธฐ ๋๋ฌธ์
- (3) ResponseFields, ์ฆ ResponseBody์ ์๋ ํ๋๊ฐ์ ๋ํ ๋ช
์ธํฉ๋๋ค.
- ํ๋๋
fieldWithPath
๋ก ์ง์ ํ ์ ์๊ณ , ์ค๋ช ๋๋ ํ์ ๋๋ optional ์ฌ๋ถ๋ ์ง์ ํด์ค ์ ์์ต๋๋ค.
- ํ๋๋
์์ธํ ์ฌํญ์ ๊ณต์๋ฌธ์๋ฅผ ํ์ธํ์๊ธฐ ๋ฐ๋๋๋ค.
๋น๋ ๋ฐ Snippet ํ์ธ
ํ
์คํธ๊ฐ ํต๊ณผ๊ฐ ๋๋ฉด, Maven ์ ๊ฒฝ์ฐ target/generated-snippets
Gradle ์ ๊ฒฝ์ฐ build/generated-snippets
์๋์ snippets์ด ์๊ธฐ๊ฒ ๋ฉ๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ์์ฑ๋๋ snippets์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
<output-directory>/index/curl-request.adoc
<output-directory>/index/http-request.adoc
<output-directory>/index/http-response.adoc
<output-directory>/index/httpie-request.adoc
<output-directory>/index/request-body.adoc
<output-directory>/index/response-body.adoc
๋ง๋ค์ด์ง Snippet ๋ค์ ์ฐ๊ฒฐํด์ค ์ฌ์ฉ์ ์ ์ .adoc
ํ์ผ์ src/docs/asciidoc
๊ฒฝ๋ก์ ๋ง๋ค๊ณ ์์ฑํฉ๋๋ค. (Asciidoctor User Manual ์ฐธ๊ณ )
.adoc ์์ฑ
== Feature 1 REST API
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
=== API ๊ธฐ๋ฅ๋ช
==== Request ์์ฒญ
include::{snippets}/className/methodName/http-request.adoc[]
include::{snippets}/className/methodName/request-headers.adoc[]
include::{snippets}/className/methodName/path-parameters.adoc[]
include::{snippets}/className/methodName/request-body.adoc[]
include::{snippets}/className/methodName/request-fields.adoc[]
==== Response ์๋ต
include::{snippets}/className/methodName/http-response.adoc[]
include::{snippets}/className/methodName/response-body.adoc[]
include::{snippets}/className/methodName/response-fields.adoc[]
toc:left
: Table Of Content๋ฅผ ์ฌ์ฉํ๊ณ ์์น๋ฅผ ์ผ์ชฝ์ผ๋ก ์ง์ ํฉ๋๋ค.toclevels
: toc์ ๊น์ด๋ฅผ ์ค์ ํฉ๋๋ค. h1~h4๊น์ง ํ์ํฉ๋๋ค.
์ด์ API ๋ณ๋ก .adoc
์ ์์ฑํ๋ค๋ฉด, ์ต์ข
API ๋ฌธ์๋ฅผ ์ํด index.adoc
์ ์์ฑํด์ค๋๋ค.
index.adoc ์์ฑ
asciidoctor
ํ๋ฌ๊ทธ์ธ์ index.adoc
์ด๋ผ๋ ํ์ผ์ index.html
์ผ๋ก ๋ณํํ์ฌ jar ๋ด๋ถ์ ์์น์์ผ์ค๋๋ค.
= MyApp REST API
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
// (1)
include::{docdir}/Feature_1_Api.adoc[]
include::{docdir}/Feature_2_Api.adoc[]
include::{docdir}/Feature_3_Api.adoc[]
- (1) ๊ธฐ๋ฅ๋ณ๋ก ์์ฑํด์ค
.adoc
ํ์ผ๋ค์ ์ ์ด์ค๋๋ค.
๋ฌธ์ ํจํค์ง
- ์์ฑ๋
Snippets
src/docs/asciidoc
์์ฑ๋ .adoc ํ์ผ๋ค
- Maven : clean → package ์์ผ๋ก ์งํ
- Gradle : clean → build ์์ผ๋ก ์งํ
include file not found.. ์๋ฌ ๋ฐ์..?
Task :asciidoctor
include file not found: /Users/iseunghan/workspaces/study/test-rest-docs-gradle/{snippets}/user-todo-list-api-controller-test/create_-todo/http-request.adoc …
index.adoc
์๋จ์ ์๋ ์ฝ๋ ์ถ๊ฐ!
ifndef::snippets[]
:snippets: ../../build/generated-snippets
endif::[]
Snippets์ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ง ๋ชปํ๋ ๊ฒ ๊ฐ์๋ฐ static/docs/index.html
๊ธฐ์ค์ผ๋ก Snippets์ ์๋๊ฒฝ๋ก๋ฅผ ์ง์ ํด์ฃผ๋ ๊ฒ์
๋๋ค.
์ฑ๊ณต!
์ ์์ ์ผ๋ก ์์ฑ๋ index.html
์ด์ localhost:8080/docs/index.html ๋ก ์ ๊ทผํ ์ ์์ต๋๋ค.
๋ชจ๋ ์ฝ๋๋ Github์ ์์ผ๋ ์ฐธ๊ณ ํ์๊ธธ ๋ฐ๋๋๋ค. ๊ฐ์ฌํฉ๋๋ค
REFERENCES
- https://spring.io/guides/gs/testing-restdocs/
- https://docs.spring.io/spring-restdocs/docs/current/reference/html5/
- https://stackoverflow.com/questions/68539790/configuring-asciidoctor-when-using-spring-restdoc
- https://subji.github.io/posts/2021/01/06/springrestdocsexample
- https://tecoble.techcourse.co.kr/post/2020-08-18-spring-rest-docs/
- https://techblog.woowahan.com/2597/
- https://springboot.tistory.com/26