์คํ๋ง ๋ฐฑ์๋ ๊ฐ๋ฐ์์ ๋ฌธ์ํ๋ฅผ ํ๋ค๊ณ ํ๋ฉด ์ผ๋จ RestDocs
๋ค.
์ฌํ๊ป RestDocs๋ง ์จ์๋ค.
Swagger ๋จ์ ์ผ๋ก
์ด๋ฐ ๋จ์ ์ด ์๊ธธ๋ RestDocs๋ง ์จ์๋ค.
ํ์ง๋ง ์ด๋ฒ ํํ์์ ํ๋ก ํธ์์ Swagger ๋ก ๋ฌธ์ํ ์์ ํด์ค ๊ฒ์ ์์ฒญํ๋ค.
๋จ์ ์ด ์๋ ๊ฒ์ ์๋๋๋ ๋จ์ํ ๋ณด๊ธฐ ์ข๊ณ , ์ฐ๊ธฐ ํธํ๋ค๊ณ Swagger ๋ก ๋์ด๊ฐ๋ ๊ฒ์ ์ข์ ๋ฐฉ๋ฒ์ด ์๋๋ผ ์๊ฐํ๋ค.
๊ทธ ๋ ํ์ ์ค์ ํ ๋ช ์ด Swagger ๋ RestDocs ๋ฅผ ์์ด์ ์ฐ๋ ๋ฌธ์ํ ๋ฐฉ๋ฒ์ด ์๋ค๊ณ ์๋ ค์คฌ๋ค.
์ ์ธ ์ด์ ๊ฐ ์์๋ค.
Swagger ๋ฌธ์ํ = SwaggerUI + Swagger
Swagger ์ฝ๋๋ openapi ์ฝ๋๋ก ๋ณํ๋๊ณ , Swagger UI๊ฐ ์ด openapi ์ฝ๋๋ฅผ ์ด์ฉํด์ ์๊ฐํํ๋ ๋ฐฉ์
์์ ์ค๋ช ํ Swagger์ ๋จ์ ์ ์์ Swagger ์ ํด๋นํ๋ ์ด์ผ๊ธฐ๋ค.
๋ค์ ๋งํด์, ์ฐ๋ฆฌ๋ SwaggerUI ๋ง ์ฐ๊ณ , Swagger ๋ ๋ฒ๋ฆฌ๋ฉด ๋๋ค๋ ์ด์ผ๊ธฐ
Swagger์ RestDocs ๋ฅผ ํจ๊ป ์ด๋ค๋ ๊ฒ์ ์ด๋ฐ ๊ณผ์ ์ผ๋ก ์งํ๋๋ค.
๊ทธ๋ผ ํ์ํ ๊ฒ ๋ฌด์์ธ์ง ๋์จ๋ค.
์ 4๊ฐ์ ์์กด์ฑ์ ์ถ๊ฐํด์ ๋ฌธ์ํ๋ฅผ ํด์ฃผ๋ฉด ๋๋ค.
์คํ๋ง์์ ํ ์คํธ์ ๋ฌธ์ํ๋ฅผ ๋ชจ๋ ์ง์ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
MockMvc
, Rest Assured
, WebTestClient
๊ฐ์ด ๊ฐ์ ํน์์๋ ๋ฐฉ๋ฒ์ ํตํด์ ํ
์คํธ/๋ฌธ์ํ๋ฅผ ์ํํ ์ ์๋ค.
๋ฌธ์ํ๋ฅผ ์ํํ๋ฉด build ํจํค์ง ๋ด์ ์ค๋ํซ์ด ๋ง๋ค์ด์ง๊ฒ ๋๊ณ ,
์ด ์ค๋ํซ์ ํ์ฉํด์ asciidoctor ๊ฐ html ํ์ผ์ ๋ง๋ค์ด ๋ ๋๋งํ๊ฒ ๋๋ค.
์ฅ์ ์ผ๋ก๋
๋จ์ ์ผ๋ก๋
์ฝ๋๋ฅผ ๊ฐ์ ธ์ค๊ธด ํ๋๋ฐ
์คํ๋ง ๋ถํธ ์ฌ์ฉํ๋ฉด initializer ๋ก ์ถ๊ฐํด์ ํ๋ฉด ํธํ๋ค.
dependencies {
// RestDocs
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
๋๋ MockMvc ๋ก ๋ฌธ์ํ๋ ํ ์คํธ๋ฅผ ํ๋ค.
์๋ ๋ณด์ด๋ ๊ฒ์ฒ๋ผ
@AutoConfigureMockMvc
, @AutoConfigureRestDocs
์ด๋
ธํ
์ด์
์ผ๋ก RestDocs MockMvc ํ
์คํธ ์ค์ ์ ํด์ค๋ค.@AutoConfigureMockMvc
@AutoConfigureRestDocs
@SpringBootTest
@Transactional
class ReviewRestControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private TestConfig testConfig;
@Test
@DisplayName("๋ฆฌ๋ทฐ๋ฅผ ๋ฑ๋กํ ์ ์๋ค๋ฉด ๋ฆฌ๋ทฐ ๋ฑ๋ก ๊ฐ๋ฅํจ์ ์๋ตํ๋ค.")
void getReviewable_NotExistsReview_Reviewable() throws Exception {
// Given
final Long academyId = 1L;
// When
ResultActions perform = mockMvc.perform(get("/reviews/reviewable")
.param("academyId", String.valueOf(academyId))
.header(AUTHORIZATION_HEADER, BEARER + testConfig.getJwt())
.accept(APPLICATION_JSON_VALUE)
.contentType(APPLICATION_JSON_VALUE));
// Then
perform.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON_VALUE))
.andExpect(jsonPath("$.academyId").value(1L))
.andExpect(jsonPath("$.reviewable").value(true))
.andDo(document("get-reviewable",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestHeaders(
headerWithName("Authorization").description("JWT ํ ํฐ (Bearer)")
),
queryParameters(
parameterWithName("academyId").description("ํ์ ์์ด๋")
),
responseFields(
fieldWithPath("academyId").type(NUMBER).description("ํ์ ์์ด๋"),
fieldWithPath("reviewable").type(BOOLEAN).description("๋ฆฌ๋ทฐ ๋ฑ๋ก ๊ฐ๋ฅ ์ฌ๋ถ")
)
));
}
}
Spring RestDocs ๋ก ๋ง๋ค์ด์ง ์ค๋ํซ์ OpenAPI3 ์คํ์ผ๋ก ๋ง๋ค์ด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
์ฐธ๊ณ ๋ก ์คํ๋ง ๋ฒ์ , ์คํ๋ง ๋ถํธ ๋ฒ์ , RestDocs ๋ฒ์ , restdocs-api-spec ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฒ์ ์ค ์ ๋ง๋๊ฒ ์์ผ๋ฉด openapi3 ์คํ์ด ๋ง๋ค์ด์ง์ง ์๋๋ค.
buildscript {
ext {
restdocsApiSpecVersion = '0.17.1'
}
}
dependencies {
// Spring Rest Api Spec
testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"
}
openapi3 ์คํ์ ๋ง๋ค์ด์ฃผ๊ธฐ ์ํด์ ์ถ๊ฐ์ ์ธ ์ค์ ์ด ํ์ํ๋ค.
openapi3 {
servers = [
{ url = "http://localhost:8080" },
{ url = "https://์๋ฒurl" }
]
title = "ํ์ ์ ๋ชฉ"
description = "ํ์ ์ค๋ช
"
version = "๋ฒ์ "
format = "json" // (json / yaml)
outputDirectory = "src/main/resources/static"
outputFileNamePrefix = "swagger"
}
Gradle openapi3 ํ์คํฌ๋ฅผ ๋ง๋ค์ด์ค๋ค.
server = โurlโ
์ด๋ ๊ฒ ํ ์๋ฒ๋ง ์ค์ ํ ์๋ ์๋ค.json
๊ณผ yml
์ค์ ์ ํํ ์ ์๋ค.์ด์ ๋ง๋ค์ด์ง openAPI3 ์คํ์ Swagger UI ๋ฅผ ํตํด HTML ๋ก ํ์ถํด์ฃผ์.
springfox ๊ทธ๋ฃน๊บผ ์ฐ๋ ์ฌ๋ ์๋๋ฐ, ๊ทธ ์์กด์ฑ ์ธ ๊ฑฐ๋ฉด ๋ฒ์ 3์ด์์ ์จ์ผ ํจ
// Swagger UI
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
์ฝํ ํธ ํ์ ์ json์ผ๋ก ์ค์ ํ๋ค.
์์ openapi3 ์ค์ ์์ json์ผ๋ก ์ค์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
swagger UI์ ๋ํ ๊ธฐ๋ณธ url ์ ์ค์ ํ๊ณ , ์ด url์ ๋ํ ์ ๊ทผ url์ ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค.
springdoc:
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
swagger-ui:
url: /v3/api-docs
path: /docs/swagger
๊ธฐ๋ณธ์ ์ผ๋ก ์์ ์ค๋ช ํ RestDocs ํ ์คํธ ์ฝ๋๋ฅผ ๋ง๋ค๋ฉด ๋๋ค.
๋ณ๊ฒฝ์ ์ด ๊ฑฐ์ ์๊ธฐ ๋๋ฌธ.
๋ฌธ์ํ ํด๋์ค ๋ณ๊ฒฝ
// Before
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
// After
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
๋ฌธ์ํ ์ฝ๋ ์์ (ํ์ ์๋; ์ ํด๋ ๋์๊ฐ)
// Before
.andDo(document("post-like",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestHeaders(
headerWithName("Authorization").description("JWT ํ ํฐ (Bearer)")
),
requestFields(
fieldWithPath("academyId").type(NUMBER).description("ํ์ ์์ด๋")
),
responseFields(
fieldWithPath("likeId").type(NUMBER).description("์ข์์ ์์ด๋"),
fieldWithPath("memberId").type(NUMBER).description("ํ์ ์์ด๋"),
fieldWithPath("academyId").type(NUMBER).description("ํ์ ์์ด๋")
)
));
// After
.andDo(document("post-like",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
resource(ResourceSnippetParameters.builder()
.tag(TAG)
.summary("์ข์์ ๋ฑ๋ก")
.requestHeaders(
headerWithName("Authorization").description("JWT ํ ํฐ (Bearer)")
)
.requestFields(
fieldWithPath("academyId").type(NUMBER).description("ํ์ ์์ด๋")
)
.responseFields(
fieldWithPath("likeId").type(NUMBER).description("์ข์์ ์์ด๋"),
fieldWithPath("memberId").type(NUMBER).description("ํ์ ์์ด๋"),
fieldWithPath("academyId").type(NUMBER).description("ํ์ ์์ด๋")
)
.build()
)
));
์ค์ ํ swagger UI url ๊ฒฝ๋ก๋ก ์ ๊ทผํ๋ฉด ๋๋ค.
http(s)://๋๋ฉ์ธ/docs/swagger
http://localhost:8080/docs/swagger
์ yml ํ์ผ์์ ์ค์ ํ ๊ฒ์ฒ๋ผ ํด๋น url๋ก ์ ๊ทผํ๋ฉด swagger ui ๋ฅผ ๋ณผ ์ ์๋ url๋ก ์ฐ๊ฒฐ๋๋ค.