Maven + MockMvc 환경에서 Spring Rest Docs 설정하기
- Coding/Spring
- 2022. 9. 14.
SpringBoot + Maven 프로젝트 생성
초기 셋팅 - 의존성
▶ pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
문서 작성을 위한 API 개발
▶ UserController
package com.maven.restapidocs.controller;
import com.maven.restapidocs.dto.ResponseDto;
import com.maven.restapidocs.dto.UserDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Slf4j
public class UserController {
@GetMapping("/{userId}")
public ResponseDto<UserDto> getUser(@PathVariable String userId){
UserDto userDto = new UserDto();
userDto.setUserId(userId);
userDto.setUserName("테스트");
userDto.setAge(27);
return ResponseDto.<UserDto>builder()
.status(HttpStatus.OK.value())
.message("SUCCESS")
.data(userDto)
.build();
}
}
▶ ResponseDto
package com.maven.restapidocs.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ResponseDto<T> {
private int status;
private String message;
private T data;
}
▶ UserDto
package com.maven.restapidocs.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserDto {
private String userId;
private String userName;
private int age;
}
테스트 코드 작성
▶ UserControllerTest.java
package com.maven.restapidocs.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import static com.maven.restapidocs.DocumentUtils.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureRestDocs
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void getUser() throws Exception {
this.mockMvc.perform(RestDocumentationRequestBuilders.get("/user/{userId}", "seohae0001")
)
.andExpect(status().isOk())
.andDo(document("get-user", getDocumentResponse(),
pathParameters(
parameterWithName("userId").description("사용자 아이디").attributes(getValidationAttribute("길이 : 8 ~ 12자" + newLine() + "조합 : 영문 + 숫자"))
),
responseFields(
fieldWithPath("status").type(JsonFieldType.NUMBER).description("결과코드"),
fieldWithPath("message").type(JsonFieldType.STRING).description("결과메시지"),
fieldWithPath("data.userId").type(JsonFieldType.STRING).description("유저아이디").attributes(getExampleAttribute("예) seohae1234")),
fieldWithPath("data.userName").type(JsonFieldType.STRING).description("유저명").attributes(getExampleAttribute("예) 김서해")),
fieldWithPath("data.age").type(JsonFieldType.NUMBER).description("나이")
)
));
}
}
1) pathParameters 설정
pathParameters(
parameterWithName("userId").description("사용자 아이디").attributes(getValidationAttribute("길이 : 8 ~ 12자" + newLine() + "조합 : 영문 + 숫자"))
),
2) responseFields 설정
responseFields(
fieldWithPath("status").type(JsonFieldType.NUMBER).description("결과코드"),
fieldWithPath("message").type(JsonFieldType.STRING).description("결과메시지"),
fieldWithPath("data.userId").type(JsonFieldType.STRING).description("유저아이디").attributes(getExampleAttribute("예) seohae1234")),
fieldWithPath("data.userName").type(JsonFieldType.STRING).description("유저명").attributes(getExampleAttribute("예) 김서해")),
fieldWithPath("data.age").type(JsonFieldType.NUMBER).description("나이")
)
▶ UserControllerTest.java
package com.maven.restapidocs;
import org.springframework.restdocs.operation.preprocess.OperationPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
import org.springframework.restdocs.operation.preprocess.Preprocessors;
import org.springframework.restdocs.snippet.Attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
public interface DocumentUtils {
/**
* request pretty
* @return
*/
static OperationRequestPreprocessor getDocumentRequest() {
return Preprocessors.preprocessRequest(new OperationPreprocessor[]{Preprocessors.prettyPrint()});
}
/**
* response pretty
* @return
*/
static OperationResponsePreprocessor getDocumentResponse() {
return Preprocessors.preprocessResponse(new OperationPreprocessor[]{Preprocessors.prettyPrint()});
}
/**
* Text 줄바꿈
* @return
*/
static String newLine() {
return " +" + "\n";
}
/**
* example 속성
* @param example
* @return
*/
static Attributes.Attribute getExampleAttribute(Object example) {
return key("example").value(example);
}
/**
* validation 속성
* @param validation
* @return
*/
static Attributes.Attribute getValidationAttribute(Object validation) {
return key("validation").value(validation);
}
}
1) Json pretty 설정
/**
* request pretty
* @return
*/
static OperationRequestPreprocessor getDocumentRequest() {
return Preprocessors.preprocessRequest(new OperationPreprocessor[]{Preprocessors.prettyPrint()});
}
/**
* response pretty
* @return
*/
static OperationResponsePreprocessor getDocumentResponse() {
return Preprocessors.preprocessResponse(new OperationPreprocessor[]{Preprocessors.prettyPrint()});
}
2) Docs 문서 안에 줄바꿈 처리는 아래와 같이 처리
static String newLine() {
return " +" + "\n";
}
문서를 생성해보자!
[1] 테스트 코드 실행
[2] 테스트 성공
[3] target/generated-snippets 경로에 .adoc 파일 생성 여부 확인
[.adoc]
- curl-request.adoc
[source,bash]
----
$ curl 'http://localhost:8080/user/seohae0001' -i -X GET
----
- http-request.adoc
[source,http,options="nowrap"]
----
GET /user/seohae0001 HTTP/1.1
Host: localhost:8080
----
- http-response.adoc
[source,http,options="nowrap"]
----
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 137
{
"status" : 200,
"message" : "SUCCESS",
"data" : {
"userId" : "seohae0001",
"userName" : "테스트",
"age" : 27
}
}
----
- httpie-request.adoc
[source,bash]
----
$ http GET 'http://localhost:8080/user/seohae0001'
----
- path-parameters.adoc
|===
|Path|Required|Description|Validation
|`+userId+`
|true
|사용자 아이디
|길이 : 8 ~ 12자 +
조합 : 영문 + 숫자
|===
- request-body.adoc
[source,options="nowrap"]
----
----
- response-body.adoc
[source,options="nowrap"]
----
{
"status" : 200,
"message" : "SUCCESS",
"data" : {
"userId" : "seohae0001",
"userName" : "테스트",
"age" : 27
}
}
----
- response-fields.adoc
|===
|Path|Type|Required|Description|Example
|`+status+`
|`+Number+`
|true
|결과코드
|
|`+message+`
|`+String+`
|true
|결과메시지
|
|`+data.userId+`
|`+String+`
|true
|유저아이디
|예) seohae1234
|`+data.userName+`
|`+String+`
|true
|유저명
|예) 김서해
|`+data.age+`
|`+Number+`
|true
|나이
|
|===
[4] src/main/asciidoc 경로에 .adoc 파일 수기 생성 (API Docs 문서의 형식 셋팅)
[5] get-user.adoc 파일 수기 생성
= API : 사용자 정보 조회
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[overview]]
== Overview
operation::get-user[snippets='curl-request']
[[overview-request-dto]]
=== Request
operation::get-user[snippets='path-parameters']
=== Request Example
operation::get-user[snippets='http-request']
[[overview-response-dto]]
=== Response
operation::get-user[snippets='response-fields']
=== Response Example
operation::get-user[snippets='response-body']
■ snippets='.aodc 파일명'을 적으면 된다.
■ operation::get-user 에서 'get-user'은 아래 테스트 코드 작성시의 identifier와 동일하게 작성한다.
[6] 크롬 아이콘을 클릭해보자.
[7] 문서 구조 확인
html 파일 생성
우리가 위에서 생성한 .adoc 파일 기준으로 .html 파일을 생성해보자.
아래 2가지 경로에 .html 파일이 생성된다.
1) target/generated-docs
2) target/classes/static/docs
Docs 안의 snippet 커스텀 생성하기
기존에 제공되는 default-**.snppet은 아래의 경로에서 확인 가능하다.
org.springframework.restdocs.templates.asciidoctor
커스텀할 snippet 파일을 아래의 경로에 추가하여 default snippet 대신, 내가 원하는 형식의 snippet으로 생성되도록 설정해보자.
경로 생성 : src/test/resources/org/springframework/restdocs/templates/asciidoctor
▶ path-parameters.snippet
|===
|Path|Required|Description|Validation
{{#parameters}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#validation}}{{.}}{{/validation}}{{/tableCellContent}}
{{/parameters}}
|===
설정 로직
1) UserControllerTest.java
pathParameters(
parameterWithName("userId").description("사용자 아이디").attributes(getValidationAttribute("길이 : 8 ~ 12자" + newLine() + "조합 : 영문 + 숫자"))
),
2) DocumentUtils.java
/**
* validation 속성
* @param validation
* @return
*/
static Attributes.Attribute getValidationAttribute(Object validation) {
return key("validation").value(validation);
}
key에 셋팅된 "validation"이 바로 위 snippet 문서의 문법 중 validation 영역에 해당된다.
3) 생성된 문서 영역
▶ response-fields.snippet
|===
|Path|Type|Required|Description|Example
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#example}}{{.}}{{/example}}{{/tableCellContent}}
{{/fields}}
|===
설정 로직
1) UserControllerTest.java
fieldWithPath("data.userId").type(JsonFieldType.STRING).description("유저아이디").attributes(getExampleAttribute("예) seohae1234")),
fieldWithPath("data.userName").type(JsonFieldType.STRING).description("유저명").attributes(getExampleAttribute("예) 김서해")),
2) DocumentUtils.java
/**
* example 속성
* @param example
* @return
*/
static Attributes.Attribute getExampleAttribute(Object example) {
return key("example").value(example);
}
3) 생성된 문서 영역
문서의 Json Pretty 설정하기
Before
■ Test 코드
■ 문서 내용
After
■ Test 코드
■ 문서 내용
Response List 객체
UserList 조회 API 추가해보자.
▶ UserController.java
...
@GetMapping("/users")
public ResponseDto<List<UserDto>> getUsers(){
UserDto userDto = new UserDto();
userDto.setUserId("test1");
userDto.setUserName("테스트1");
userDto.setAge(27);
UserDto userDto2 = new UserDto();
userDto2.setUserId("test2");
userDto2.setUserName("테스트2");
userDto2.setAge(25);
List<UserDto> list = new ArrayList<>();
list.add(userDto);
list.add(userDto2);
return ResponseDto.<List<UserDto>>builder()
.status(HttpStatus.OK.value())
.message("SUCCESS")
.data(list)
.build();
}
...
▶ UserControllerTest.java
...
@Test
void getUserList() throws Exception {
this.mockMvc.perform(RestDocumentationRequestBuilders.get("/user/users")
)
.andExpect(status().isOk())
.andDo(document("get-users", getDocumentResponse(), responseFields(
fieldWithPath("status").type(JsonFieldType.NUMBER).description("결과코드"),
fieldWithPath("message").type(JsonFieldType.STRING).description("결과메시지"),
fieldWithPath("data[].userId").type(JsonFieldType.STRING).description("유저아이디").attributes(getExampleAttribute("예) seohae1234")),
fieldWithPath("data[].userName").type(JsonFieldType.STRING).description("유저명").attributes(getExampleAttribute("예) 김서해")),
fieldWithPath("data[].age").type(JsonFieldType.NUMBER).description("나이")
)));
}
...
1) 리스트의 경우 []를 추가한다.
fieldWithPath("data[].userId").type(JsonFieldType.STRING).description("유저아이디").attributes(getExampleAttribute("예) seohae1234")),
fieldWithPath("data[].userName").type(JsonFieldType.STRING).description("유저명").attributes(getExampleAttribute("예) 김서해")),
fieldWithPath("data[].age").type(JsonFieldType.NUMBER).description("나이")
▶ get-users.adoc
= API : 사용자 정보 조회
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:curl-request.adoc
[[overview]]
== Overview
operation::get-users[snippets='curl-request']
[[overview-request-dto]]
=== Request
operation::get-users[snippets='path-parameters']
=== Request Example
operation::get-users[snippets='http-request']
[[overview-response-dto]]
=== Response
operation::get-users[snippets='response-fields']
=== Response Example
operation::get-users[snippets='response-body']
▶ 문서 생성 모습
'Coding > Spring' 카테고리의 다른 글
DispatcherServlet 요청 흐름 (0) | 2022.12.13 |
---|---|
Spring + Mybatis 활용 원리 (1) | 2022.12.01 |
SpringBoot + Validation 프로젝트 설정 (커스텀 Annotation 적용 방법, DTO 필드에 constraints 어노테이션 적용, Validator 인터페이스 구현, constraints 어노테이션에 groups 속성 설정) (0) | 2022.08.25 |
SpringBoot + SpringSecurity 프로젝트에 Swagger 3.0 적용하기 (2) | 2022.02.02 |
SpringBoot 프로젝트 + Postgresql 연동 및 Mac Postgresql 설치 과정 (0) | 2022.01.16 |