plugins {
id 'java'
id 'org.springframework.boot' version '3.0.1'
id 'io.spring.dependency-management' version '1.1.0'
// Asciidoctor 플러그인 적용
// gradle 7.0 이상부터는 jvm 사용
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
group = 'com.daengnyangffojjak'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
asciidoctorExtensions // dependencies 에서 적용한 것 추가
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
...
implementation 'org.springframework.security:spring-security-test:6.0.1' //버전확인
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// (1)
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
// (2)
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testCompileOnly 'org.projectlombok:lombok:1.18.24' // 테스트 의존성 추가
testAnnotationProcessor 'org.projectlombok:lombok:1.18.24' // 테스트 의존성 추가
}
tasks.named('test') {
useJUnitPlatform()
}
ext {
// 아래서 사용할 변수 선언
snippetsDir = file('build/generated-snippets')
}
test {
// 위에서 작성한 snippetsDir 디렉토리를 test의 output으로 구성하는 설정 -> 스니펫 조각들이 build/generated-snippets로 출력
outputs.dir snippetsDir
useJUnitPlatform()
}
// asciidoctor 설정
asciidoctor {
dependsOn test // test 작업 이후에 작동하도록 하는 설정
configurations 'asciidoctorExtensions' // 위에서 작성한 configuration 적용
inputs.dir snippetsDir // snippetsDir 를 입력으로 구성
// source가 없으면 .adoc파일을 전부 html로 만들어버림
// source 지정시 특정 adoc만 HTML로 만든다.
sources{
include("**/index.adoc","**/common/*.adoc")
}
// 특정 .adoc에 다른 adoc 파일을 가져와서(include) 사용하고 싶을 경우 경로를 baseDir로 맞춰주는 설정입니다.
// 개별 adoc으로 운영한다면 필요 없는 옵션입니다.
baseDirFollowsSourceFile()
}
// asciidoctor가 실행될 때 static/docs 폴더 비우기
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
// asccidoctor 작업 이후 생성된 HTML 파일을 static/docs 로 copy
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
// build 시 codyDocument 실행
build {
dependsOn copyDocument
}
//build의 bootJar 시 asciidoctor에 의존하여 /static/docs에 index.html이 생성된다.
bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
(1) spring-restdocs-asciidoctor
build/generated-snippets 에 생긴 .adoc 조각들을 프로젝트 내의 .adoc 파일에서 읽어들일 수 있도록 연동해준다.
이 덕분에 .adoc 파일에서 operation 같은 매크로를 사용하여 스니펫 조각들을 연동할 수 있다.
그리고 최종적으로 .adoc 파일을 HTML로 만들어 export 해준다.
(2) spring-restdocs-mockmvc
restdocs-mockmvc의 testCompile 구성 -> mockMvc를 사용해서 snippets 조각들을 뽑아낼 수 있게 된다.
MockMvc 대신 WebTestClient을 사용하려면 spring-restdocs-webtestclient 추가.
MockMvc 대신 REST Assured를 사용하려면 spring-restdocs-restassured 를 추가.
@PostMapping(value = "/join") //회원가입
public ResponseEntity<Response<UserJoinResponse>> join(@RequestBody UserJoinRequest request){
UserJoinResponse userJoinResponse = userService.join(request);
return ResponseEntity.created(URI.create("/api/v1/users/"+userJoinResponse.getId())) //성공 시 상태코드 : 201
.body(Response.success(userJoinResponse));
}
.acod 파일 형식을 설정하고, 테스트 작성을 편리하게 하기 위해서 몇가지 설정을 추가한다.
(1) .adoc 파일이 저장될 폴더명을 클래스이름-메소드명으로 자동으로 지정해준다.
(2), (3) response, request body의 JSON이 보기 좋게 이쁘게 출력된다.
(4) 커스텀으로 넣는 컬럼을 넣기 편하게 사용할 메서드이다.
@TestConfiguration
public class RestDocsConfiguration {
@Bean
public RestDocumentationResultHandler write(){
return MockMvcRestDocumentation.document(
"{class-name}/{method-name}", // (1)
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), // (2)
Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) // (3)
);
}
public static final Attribute field( // (4)
final String key,
final String value){
return new Attribute(key,value);
}
}
@WebMvcTest(UserRestController.class)
@AutoConfigureRestDocs
@WithMockUser
@Import(RestDocsConfiguration.class)
@ExtendWith(RestDocumentationExtension.class)
class UserRestControllerTest {
@MockBean
UserService userService;
@Autowired
ObjectMapper objectMapper;
@Autowired
protected RestDocumentationResultHandler restDocs;
@Autowired
private MockMvc mockMvc;
UserJoinRequest userJoinRequest = new UserJoinRequest("hoon", "hi", "gg@gmail.com");
@BeforeEach
void setUp(final WebApplicationContext context,
final RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider)) // rest docs 설정 주입
.alwaysDo(MockMvcResultHandlers.print()) // andDo(print()) 코드 포함
.alwaysDo(restDocs) // pretty 패턴과 문서 디렉토리 명 정해준것 적용
.addFilters(new CharacterEncodingFilter("UTF-8", true)) // 한글 깨짐 방지
.build();
}
@Nested
@DisplayName("회원가입")
class join{
@Test
@DisplayName("회원가입 성공")
void join_success() throws Exception {
given(userService.join(userJoinRequest)).willReturn(new UserJoinResponse(0L, "hoon", "gg@gmail.com"));
mockMvc.perform(
post("/api/v1/users/join")
.content(objectMapper.writeValueAsBytes(userJoinRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.result.userName").value("hoon"))
.andExpect(jsonPath("$.result.id").value(0))
.andDo(
restDocs.document(
requestFields(
fieldWithPath("userName").description("유저아이디").attributes(field("constraints", "중복 불가능")),
fieldWithPath("password").description("비밀번호"),
fieldWithPath("email").description("이메일").attributes(field("constraints", "중복 불가능"))
),
responseFields(
fieldWithPath("resultCode").description("결과코드"),
fieldWithPath("result.id").description("유저번호"),
fieldWithPath("result.userName").description("유저아이디"),
fieldWithPath("result.email").description("이메일"))
)
);
verify(userService).join(userJoinRequest);
}
}
}
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ExtendWith(RestDocumentationExtension.class)
📌 실제 작성 시 신경 쓸 부분
.andDo(
restDocs.document(
requestFields(
fieldWithPath("userName").description("유저아이디").attributes(field("constraints", "중복 불가능")),
fieldWithPath("password").description("비밀번호"),
fieldWithPath("email").description("이메일").attributes(field("constraints", "중복 불가능"))
),
responseFields(
fieldWithPath("resultCode").description("결과코드"),
fieldWithPath("result.id").description("유저번호"),
fieldWithPath("result.userName").description("유저아이디"),
fieldWithPath("result.email").description("이메일"))
)
);
예시 1 - get
.andDo(
restDocs.document(
pathParameters(
parameterWithName("id").description("Member ID")
),
responseFields(
fieldWithPath("id").description("ID"),
fieldWithPath("age").description("age"),
fieldWithPath("email").description("email")
)
)
)
예시 2 - get
.andDo(
restDocs.document(
requestParameters(
parameterWithName("size").optional().description("size"),
parameterWithName("page").optional().description("page")
)
)
)
예시 3 - modify
.andDo(
restDocs.document(
pathParameters(
parameterWithName("id").description("Member ID")
),
requestFields(
fieldWithPath("age").description("age")
fieldWithPath("email").optional().description("email")
)
)
)
테스트 작성 후 테스트를 통과하게 되면 아래 경로에 adoc 조각 파일들이 생성된다.
기본적으로 다음과 같은 조각들이 default로 생성된다.
테스트 코드에 따라 추가적인 조각이 생성될 수 있다.
이제 이 조각들로 문서를 작성한다.
= REST Docs (글의 제목)
(부제)
:doctype: book
:icons: font
:source-highlighter: highlightjs // 문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용
:toc: left // toc (Table Of Contents)를 문서의 좌측에 두기
:toclevels: 2
:sectlinks:
[[User-API]]
== User API
[[User-회원가입]]
=== User 회원가입
operation::join/join_success[snippets='http-request,request-fields,http-response,response-fields']
gradle - build - build를 더블 클릭하여 빌드를 한다.
resources - static - docs 안에 index.html이 생성된다.
http://localhost:8080/docs/index.html 으로 접속하면 완성된 문서를 확인할 수 있다.
문서 분리할 부분에 커서를 놓고 option + enter (윈도우는 alt + enter?)를 누르면, Extract Include Directive가 나온다.
이를 선택하면 같은 폴더 내로 adoc 파일을 분리해 준다.
📌 추가로 필요한 기능들은 추후 업데이트하겠습니다.
Factory method 'mockMvc' threw exception with message: javax/servlet/http/HttpServletResponse
java.lang.ClassNotFoundException: javax.servlet.http.HttpServletResponse
classpath를 못 찾고 있음
⭕️ dependency들 버전을 높여보자
spring security test 버전 5.7.x에서 6.0.x로 올리니 실행되었다.
implementation 'org.springframework.security:spring-security-test:6.0.1'
참고 :
https://velog.io/@backtony/Spring-REST-Docs-적용-및-최적화-하기#2-swagger-vs-rest-docs