Java/Spring & Spring Boot

Spring Rest Docs Custom

마손리 2023. 5. 7. 00:23

다이전 포스트에서 Spring Rest Docs를 이용하여 API문서의 자동화를 진행하던 중 몇가지 보완해야될 점을 발견했다.

  1. 테이블에 optional에 대한 정보 표현
  2. 유효성 검증에 대한 정보 표현

두가지를 표현하기 위해서 Spring Rest Docs 사용자 정의를 이용하여 API문서를 좀 더 개발자에 맞게 표현해줄 수 있다.

 

Optional에 대한 정보 표현

1. Request fields의 Optional에 대한 정보 표시

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
	...
    ...

    @Test
    public void patchMemberTest() throws Exception {
    	...
        ...

        // then
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.memberId").value(patch.getMemberId()))
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.phone").value(patch.getPhone()))
                .andExpect(jsonPath("$.data.memberStatus").value(patch.getMemberStatus().getStatus()))
                .andDo(document("patch-member",
                        getRequestPreProcessor(),
                        getResponsePreProcessor(),
                        pathParameters(
                                parameterWithName("member-id").description("회원 식별자")
                        ),
                        requestFields(
                                List.of( // (1)
                                        fieldWithPath("memberId").type(JsonFieldType.NUMBER)
                                                .description("회원 식별자").ignored(),
                                        fieldWithPath("name").type(JsonFieldType.STRING)
                                                .description("이름").optional(),
                                        fieldWithPath("phone").type(JsonFieldType.STRING)
                                                .description("휴대폰 번호").optional(),
                                        fieldWithPath("memberStatus").type(JsonFieldType.STRING)
                                                .description("회원 상태: MEMBER_ACTIVE / MEMBER_SLEEP / MEMBER_QUIT").optional()
                                )
                        ),
                        ...
                        ...
                ));
    }
}

Controller의 Patch request test 코드의 일부이다. 

 

코드의 (1)번과 같이 "name", " phone", "memberStatus" 라는 필드에 optional()메서드가 들어가 필수값이 아님을 명시해 주고 있다.

 

하지만 테스트 후, 실제 request-fields.adoc 파일에는 해당 optional의 정보(컬럼)이 들어 있지 않다.

 

optional 정보를 해당 테이블에 표현해 주기 위해서 사용자 정의로 테이블을 따로 설계 해주어야 한다.

 

1. 해당 스니펫에 대한 템플릿 생성

  • src/test/resources/org/springframework/restdocs/templates 폴더 생성
  • [생성할 템플릿의 스니펫 이름].snippet 파일 생성. ex) request-fields.snippet (request-fields.adoc의 템플릿)

2. 생성된 템플릿 파일에 Mustache 문법으로 테이블 설계

|===
|필드명|타입|필수값 여부|설명

{{#fields}} // (1)
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} // (2)
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}

|===

 

위와 같이 request-fields.snippet에 Mustache문법을 작성해 준다.

 

  • 위의 테스트 코드에서 필드의 이름, 타입, 설명, 옵션 여부의 정보를 제공해준다. 생성할 API문서의 테이블에서 해당 필드들의 정보를 담을 컬럼들을 (1)번에서 정의해 준다.
  • 테스트 코드에서 fieldWithPath() 메서드를 사용하므로 (2)번에서 {{#fields}}를 설정해주어 필드로 지정된 값들만을 표현해준다. 
  • (1)번에서 정한 컬럼의 순서대로 표현할 값들을 순서대로 정의해 준다. fieldWithPath() 메서드에서 필드의 이름은 (3)번과 같이 {{path}}로 지정해줄 수 있다. 
  • Mustache문법에서 '#'은 if문과 같이 어떠한 값이 있을 경우를 뜻하며 '^'어떠한 값이 없을 경우를 뜻한다. (4)번과 같이 {{^optional}}을 사용하면 optional이 아닌 필드들은 true를 명시해준다. 

이후 해당 테스트 코드를 실행하면 사용자 정의로 변경된 request-fields.adoc가 생성된다.

 

생성된 request-fields.adoc 결과

테스트전, 작성한 템플릿 request-fields.snippet에 모든 주석처리는 지워준다.

모든 필드들이 optional 이므로 해당 컬럼은 비어저서 표현된다.

 

 

2. Query parameter의 Optional에 대한 정보 표시

Get request의 테스트코드

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
	...
    ...
    @Test
    public void getMembersTest() throws Exception {
    	...
    	...
        //then
        mockMvc.perform(
                        get("/v11/members")
                                .params(params)
                ).andExpect(status().isOk())
                .andExpect(jsonPath("$.pageInfo.page").value(page))
                .andExpect(jsonPath("$.pageInfo.size").value(members.getSize()))
                .andExpect(jsonPath("$.pageInfo.totalElements").value(members.getTotalElements()))
                .andExpect(jsonPath("$.pageInfo.totalPages").value(members.getTotalPages()))
                .andDo(document("get-members",
                        preprocessRequest(prettyPrint()),  
                        preprocessResponse(prettyPrint()),
                        requestParameters( // (1)
                                parameterWithName("page").description("출력될 페이지 위치"),
                                parameterWithName("size").description("한 페이지당 출력될 최대 컨텐츠의 수")
                        ),
                        responseFields(
                                List.of(
                                	...
                                	...     
                                )
                        )
                ));
    }
}

 

Get request에 대한 API 문서의 쿼리 파라미터에 대한 정보 (path-parameters.adoc)

 

 

 

테스트 코드에서 (1)번과 같이 Get request는 2개의 파라미터를 받아오며 해당 쿼리 파라미터의 정보를 API 문서에 담고 있다.

 

여기에 사용자정의 템플릿을 이용하여 Patch request와 같이 필수값 여부에 대한 정보를 표현할 수 있다.

 

src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet

|===
|파라미터명|필수값 여부|설명 // (1)

{{#parameters}} // (2)
    |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} // (3)
    |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
    |{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/parameters}}

|===

 

  • 해당 테이블에 들어갈 컬럼들을 (1)번에서 명시해준다.
  • 테스트코드에서 parameterWithName() 메서드를 사용해주고 있다. (2)번과 같이 작성하여 파라미터 정보를 받아온다.
  • Patch의 경우 fieldWithPath() 메서드를 사용하므로 {{path}}로 해당 필드의 이름을 받아왔다. parameterWithName() 메서드의 경우 {{name}}으로 파라미터의 이름을 받아온다.

 

생성된 path-parameters.adoc의 결과

테스트전, 작성한 템플릿 request-parameters.snippet 모든 주석처리는 지워준다.

 

 

 

유효성 검증에 대한 정보 표현

optional과는 달리 validation에 대한 정보는 해당 Dto 클래스에서 따로 가져와야 한다.

 

MemberDto.Patch

public class MemberDto {
    @Getter
    @Builder
    public static class Patch {
        private long memberId;

	// (1)
        @NotSpace(message = "회원 이름은 공백이 아니어야 합니다")
        private String name;

        @NotSpace(message = "휴대폰 번호는 공백이 아니어야 합니다")
        @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
                message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
        private String phone;

        private Member.MemberStatus memberStatus;

        public void setMemberId(long memberId) {
            this.memberId = memberId;
        }
    }
}

Patch 요청의 Dto이다. (1)번과 같이 @NotSpace라는 사용자 정의 에너테이션@Pattern이라는 자바에서 제공하는 유효성 검증 에너테이션을 사용하고 있다.

 

@Pattern과 같이 자바에서 제공하는 에너테이션의 경우는 상관 없지만 @NotSpace와 같이 개발자가 직접 정의한 에너테이션의 경우 API 문서에 어떻게 표현될 것인지를 정의해 주어야 한다.

 

  1. src/test/resources/org/springframework/restdocs/constraints 폴더 생성
  2. ConstraintDescriptions.properties 파일 생성
// (1)                                        // (2)
com.codestates.validator.NotSpace.description=Must not be allowed space

 

생성된 파일에 위와 같이 사용자 정의 에너테이션을 사용할 경우  API 문서에 표현될 문구를 정해준다.

  • (1)번은 [해당 사용자 정의 에너테이션이 정의된 인터페이스의 위치].descrption 으로 에너테이션의 위치를 알려준다. 위의 예시의 경우 src/main/java/com/codestates/validator 폴더안에 NotSpace라는 인터페이스에 해당 에너테이션이 정의되 있다. 
  • (2)번은 해당 에너테이션의 유효성 검증 정보이며 API 문서에 표현될 문구이다.

 

 

테스트 코드 수정

이제 위와 같이 사용자가 정의한 정보 혹은 자바에서 이미 정의된 유효성 검증의 정보들을 테스트 코드에서 불러온뒤 해당 정보들을 API 문서 스니펫을 만들때 제공해 주어야한다.

 

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerDocumentationHomeworkTest_V1 {
	...
    ...
    @Test
    public void patchMemberTest() throws Exception {
    	...
        ...
        
        //then
        // (1) 유효성 검증에 사용된 애너테이션에 대한 정보를 가저옴
        ConstraintDescriptions patchMemberConstraints = new ConstraintDescriptions(MemberDto.Patch.class); // 유효성 검증 조건 정보 객체 생성
        List<String> nameDescriptions = patchMemberConstraints.descriptionsForProperty("name"); // name 필드의 유효성 검증 정보 얻기
        List<String> phoneDescriptions = patchMemberConstraints.descriptionsForProperty("phone"); // phone 필드의 유효성 검증 정보 얻기

        System.out.println(nameDescriptions);

        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.memberId").value(patch.getMemberId()))
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.phone").value(patch.getPhone()))
                .andExpect(jsonPath("$.data.memberStatus").value(patch.getMemberStatus().getStatus()))
                .andDo(document("patch-member",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        pathParameters(
                                List.of(parameterWithName("member-id").description("회원 식별자 ID")) 
                        ),
                        requestFields(
                                List.of(
                                    fieldWithPath("memberId")
                                    	.type(JsonFieldType.NUMBER)
                                        .description("회원 식별자")
                                        .ignored(),
                                    fieldWithPath("name")
                                    	.type(JsonFieldType.STRING)
                                        .description("이름")
                                        .attributes(key("constraints")//(2)유효성 검증 정보를 API 스펙으로 표현
                                        .value(nameDescriptions))
                                        .optional(), 
                                    fieldWithPath("phone")
                                    	.type(JsonFieldType.STRING)
                                        .description("휴대폰 번호")
                                        .attributes(key("constraints")//유효성 검증 정보를 API 스펙으로 표현
                                        .value(phoneDescriptions))
                                        .optional(), 
                                    fieldWithPath("memberStatus")
                                    	.type(JsonFieldType.STRING)
                                        .description("회원 상태: MEMBER_ACTIVE(활동중) / MEMBER_SLEEP(휴면 계정) / MEMBER_QUIT(탈퇴)")
                                        .optional()
                                )
                        ),
                        responseFields(
                                List.of(
                                	...
                                    ...
                                )
                        )
                ));
    }
}
  • (1)번과 같이 Spring Rest Docs에서 제공하는 ConstraintDesciptions 클래스를 이용하면 해당 Dto에 담겨있는 필드와 제약사항들의 정보를 객체로 관리할 수 있다. 

    Dto를 살펴보면 "name"과 "phone" 필드에만 유효성 검증을 하므로 해당 필드들에 대한 제약사항을 descriptionForProperty() 메서드를 사용하여 제약사항에 대한 정보를 제공 받는다.

    만약 위에서 ConstraintDescriptions.properties 파일을 정의해주지 않는다면 테스트시 이부분에서 테스트가 실패한다. 

  • 이후, (2)번과 같이 해당 필드에 attributes()메서드를 사용하여 (1)번에 저장된 해당 필드들의 유효성에대한 정보를 key 스태틱 메서드를 이용하여 key와 value의 형태로 저장한다.

 

아래와 같이 value에 직접 타이핑하여 명시해줄 수 있다.

fieldWithPath("name")
    .type(JsonFieldType.STRING)
    .description("이름")
    .attributes(key("constraints")
    .value("Must not be allowed space"))
    .optional(),

 

 

해당 스니펫에 대한 템플릿 생성 후 Mustache 문법으로 테이블을 설계

해당 예시의 경우 request-fields.adoc의 테이블에 유효성 검증을 위한 정보를 제공할 것이므로 src/test/resources/org/springframework/restdocs/templates/request-fields.snippet 파일에 제약사항이라는 컬럼을 추가해준다.

 

|===
    |필드명|타입|필수값 여부|설명|제약 조건 // (1)

{{#fields}}
    |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
    |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
    |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
    |{{#tableCellContent}}{{description}}{{/tableCellContent}}
    |{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}} // (2)
{{/fields}}

|===
  • (1)번에서 '제약 조건'이라는 컬럼을 추가
  • (2)번의 경우, 필드에 constraints (테스트 코드에서 key("constraints")로 지정)라는 key가 존재하면 해당 value의 제일 첫번째 값을 표현한다. (constraints의 value는 List로 제공되었음.)

 

생성된 request-fields.adoc의 결과

 

 

해당자료 깃헙 - 

https://github.com/Mason3144/Java_test_practiceAndExamples

 

Multypart form data 요청의 테스트와 API 문서화 - 

https://mason-lee.tistory.com/141

'Java > Spring & Spring Boot' 카테고리의 다른 글

Spring Rest Docs (API 문서화)  (0) 2023.05.03
Slice test  (0) 2023.05.01
Unit test (단위 테스트)와 테스트 종류  (0) 2023.05.01
DB N+1 문제해결(JPQL의 join fetch 사용)  (0) 2023.04.29
이벤트리스너와 비동기  (2) 2023.04.28