1. 문서 커스텀하기
@Data
public class MemberCreateRequest {
@NotNull
@Size(min = 2, max = 20)
private String name;
@Size(min = 2, max = 20)
private String nickname;
@Max(value = 200)
private int age;
}
@Data
public class PostsGetRequest {
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate date;
@Size(min = 1)
private String title;
private int page;
private int size;
public PostsGetRequest() {
page = 0;
size = 10;
}
}
spring rest docs를 이용해 메시지 바디로 들어오는 값(requestFields)을 테스트한다면 request-fields 스니펫이 생성된다. 마찬가지로 queryParameters를 테스트하면 query-parameters 스니펫이 생된된다.
(테스트에 사용된 버전은 Spring REST Docs 3.0 버전입니다. requestParameters는 삭제되었고 queryParameters를 사용해서 쿼리 파라미터를 문서화합니다.)
단, 이대로 사용하면 name이 필수값인지, 어떤 제약조건이 있는지 알 수 없다.
이를 위해 각각의 스니펫을 커스텀해줄 필요가 있다. 아래 링크는 공식문서에서 스니펫을 커스터마이징하는 방법에 관해 설명한 부분이다.
/test/resources/**/templates/request-fields.snippet
|===
|필드명|타입|필수값|기본값|제약조건|설명
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{#defaultValue}}{{.}}{{/defaultValue}}{{/tableCellContent}}
|{{#tableCellContent}}{{#constraint}}{{.}}{{/constraint}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===
/test/resources/**/templates/query-parameters.snippet
|===
|파라미터|필수값|기본값|제약조건|설명
{{#parameters}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{#defaultValue}}{{.}}{{/defaultValue}}{{/tableCellContent}}
|{{#tableCellContent}}{{#constraint}}{{.}}{{/constraint}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/parameters}}
|===
request-fields는 기본 속성인 path, type, description을 포함하고, query-parameters는 기본 속성으로 name, description을 포함한다. 필수값 여부, 기본값, 제약조건을 명시하기 위해 각각 optional, defaultValue, contraint를 추가해준다.
/test/**/DocumentFormatGenerator
import org.springframework.restdocs.snippet.Attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
public interface DocumentFormatGenerator {
static Attributes.Attribute getDateConstraint() {
return key("constraint").value("yyyy-MM-dd");
}
static Attributes.Attribute getConstraint(String value) {
return key("constraint").value(value);
}
static Attributes.Attribute getDefaultValue(String value) {
return key("defaultValue").value(value);
}
}
@WebMvcTest(PostController.class)
@AutoConfigureRestDocs
class PostControllerTest {
@Autowired
MockMvc mvc;
@Autowired
ObjectMapper mapper;
@Test
public void getPosts() throws Exception {
//given
MultiValueMap<String, String> request = new LinkedMultiValueMap<>();
request.add("date", "2023-04-10");
request.add("title", "title search");
request.add("page", "0");
request.add("size", "10");
//when
ResultActions resultActions = mvc.perform(get("/posts")
.params(request));
//then
resultActions.andExpect(status().isOk())
.andDo(print())
.andDo(document("posts-get",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
queryParameters(
parameterWithName("date")
.attributes(getDateConstraint())
.description("검색 날짜").optional(),
parameterWithName("title")
.attributes(getConstraint("1자 이상"))
.description("제목 검색").optional(),
parameterWithName("page")
.attributes(getDefaultValue("0"))
.description("페이지"),
parameterWithName("size")
.attributes(getDefaultValue("10"))
.description("페이지 사이즈")
)));
}
}
이후 attributes.key("key").value("value") 형태로 줄줄 엮어주고 테스트를 돌리면 커스텀 템플릿이 적용된 스니펫이 만들어진다.
2. 공통 응답 문서화
{
"code" : "200",
"data" : "데이터",
"number" : 0,
"size" : 1,
"totalPages" : 1,
"hasNext" : false,
"hasPrevious" : false
}
위와 같이 data에는 실제 응답 데이터가 담기고 code에는 HTTP 상태 코드, 나머지는 page에 관한 정보가 담기는 응답을 만들었다. 다만, 테스트 코드를 작성할 때마다 이를 문서화해주는 것은 번거롭다.
이를 분리해서 문서화해주기 위해 별도의 테스트용 컨트롤러를 작성하고, 기존과 동일하게 테스트 코드를 작성한다.
/test/**/DocumentController
@RestController
@RequiredArgsConstructor
public class DocumentController {
@GetMapping("/page-result")
public Result<String> getPageResult() {
return new PageResult<>("200", "데이터", new PageImpl<>(List.of("데이터")));
}
@GetMapping("/result")
public Result<String> geResult() {
return new Result<>("200", "데이터");
}
}
@Test
public void commonPageResult() throws Exception {
//given
//when
ResultActions resultActions = mvc.perform(get("/page-result")
.accept(MediaType.APPLICATION_JSON));
//then
resultActions
.andExpect(status().isOk())
.andDo(print())
.andDo(document("common-page-result",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
responseFields(
attributes(key("title").value("공통 응답(페이지 정보)")),
fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 응답 코드"),
subsectionWithPath("data").type(JsonFieldType.VARIES).description("응답 데이터"),
fieldWithPath("number").type(JsonFieldType.NUMBER).description("페이지 번호"),
fieldWithPath("size").type(JsonFieldType.NUMBER).description("페이지 사이즈"),
fieldWithPath("totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"),
fieldWithPath("hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 여부"),
fieldWithPath("hasPrevious").type(JsonFieldType.BOOLEAN).description("이전 페이지 여부")
)));
}
이렇게 공통 응답을 분리하면 테스트 코드를 작성할때 beneathPath("data") 를 이용하여 data 하위의 응답에 대해서만 작성하면 된다.
3. Enum 문서화
public enum PostCategory implements EnumType {
INFO("정보"),
QUESTION("질문"),
CHAT("잡담");
private final String text;
PostCategory(String text) {
this.text = text;
}
@Override
public String getName() {
return this.name();
}
@Override
public String getText() {
return this.text;
}
}
위와 같은 Enum이 있을 때, 이를 문서화해주기 위해서 테스트 코드에 별도의 컨트롤러를 작성해준다. 그리고 기존의 문서화 작업과 동일하게 테스트 코드를 작성한다.
@GetMapping("/post-category")
public Result<Map<String, String>> getPostCategory() {
return new Result<>("200", getEnumTypes(PostCategory.values()));
}
private Map<String, String> getEnumTypes(EnumType[] enumTypes) {
return Arrays.stream(enumTypes).collect(Collectors.toMap(EnumType::getName, EnumType::getText));
}
@Test
public void commonPostCategory() throws Exception {
//given
//when
ResultActions resultActions = mvc.perform(get("/post-category")
.accept(MediaType.APPLICATION_JSON));
//then
resultActions
.andExpect(status().isOk())
.andDo(print())
.andDo(document("custom-response",
responseFields(
beneathPath("data").withSubsectionId("post-category"),
enumConvertFieldDescriptor(PostCategory.values())
)));
}
private FieldDescriptor[] enumConvertFieldDescriptor(EnumType[] enumTypes) {
return Arrays.stream(enumTypes)
.map(enumType -> fieldWithPath(enumType.getName()).description(enumType.getText()))
.toArray(FieldDescriptor[]::new);
}
테스트를 돌리면 response-fields 스니펫은 다음과 같은 형식으로 생성된다.
여기서 좀 더 간결한 response-fields 스니펫을 만들어주기 위해 다음과 같은 CustomResponseFieldSnippet 클래스를 만들어주고, 문서화를 위한 테스트 코드 작성시 이를 사용한다.
더하여 enum type을 위한 custom-response 템플릿을 /test/resources/**/templates 하위에 추가해준다.
public class CustomResponseFieldsSnippet extends AbstractFieldsSnippet {
public CustomResponseFieldsSnippet(String type, List<FieldDescriptor> descriptors,
Map<String, Object> attributes,
boolean ignoreUndocumentedFields,
PayloadSubsectionExtractor<?> subsectionExtractor) {
super(type, descriptors, attributes, ignoreUndocumentedFields, subsectionExtractor);
}
@Override
protected MediaType getContentType(Operation operation) {
return operation.getResponse().getHeaders().getContentType();
}
@Override
protected byte[] getContent(Operation operation) throws IOException {
return operation.getResponse().getContent();
}
}
@Test
public void commonPageCategory() throws Exception {
//given
//when
ResultActions resultActions = mvc.perform(get("/post-category")
.accept(MediaType.APPLICATION_JSON));
//then
resultActions
.andExpect(status().isOk())
.andDo(print())
.andDo(document("custom-response",
customResponseFields("custom-response", attributes(key("title").value("게시글 카테고리")),
beneathPath("data").withSubsectionId("post-category"),
enumConvertFieldDescriptor(PostCategory.values()))));
}
private FieldDescriptor[] enumConvertFieldDescriptor(EnumType[] enumTypes) {
return Arrays.stream(enumTypes)
.map(enumType -> fieldWithPath(enumType.getName()).description(enumType.getText()))
.toArray(FieldDescriptor[]::new);
}
public static CustomResponseFieldsSnippet customResponseFields(String type,
Map<String, Object> attributes,
PayloadSubsectionExtractor<?> subsectionExtractor,
FieldDescriptor... descriptors) {
return new CustomResponseFieldsSnippet(type, Arrays.asList(descriptors), attributes,
true, subsectionExtractor);
}
/test/resources/**/templates/custom-response-fields.snippet
|===
|코드|코드명
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===
테스트를 돌리면 아래와 같이 간결한 스니펫을 만들어줄 수 있다.
# 참고
https://techblog.woowahan.com/2597/
'Spring' 카테고리의 다른 글
스프링과 웹소켓 (0) | 2023.05.01 |
---|---|
@Scheduled 어노테이션을 이용한 스케줄링 (0) | 2023.04.30 |
[Session] Redis 세션 클러스터링 (0) | 2023.04.15 |
[Spring REST Docs][오류] urlTemplate not found. (0) | 2023.03.26 |
[Spring REST Docs] 맛보기 (0) | 2023.03.23 |