> Hello World !!!

     

@syaku

Spring RestDocs 작성 가이드 #2 응용편 - 보일러플레이트 코드 제거

Github: https://github.com/syakuis/spring-restdocs

RestDocs 는 여러 컨트롤러에서 만들어지는 중복되는 필드 정의에 의해 생산성이 저하될 수 있다.

AS-IS

TO-BE

AS-IS 에서 표시된 영역에서 반복이 발생되어 모듈로 만들어 재사용할 수 있도록 TO-BE 와 같이 구현할 것이다.

그래서 보일러플레이트 코드를 제거하고 한번 정의된 필드를 재사용할 수 있는 방법을 가이드하였다.

사용 가이드

클래스 설명

  • Descriptor : RestDocs 사용에 맞춰 필드를 정의하는 인터페이스이다.
  • RestDocsDescriptor : 정의된 필드를 제어한다.
  • AutoConfigureMvcRestDocs : 현 API 초기 설정을 정의한 RestDocs 설정한다.

필드 정의 예시

[코드 1-1] enum 에서 필드 정의

@Getter
public enum ChangePasswordField implements Descriptor {
    currentPassword("현재 비밀번호", false),
    newPassword("새로운 비밀번호", false),
    newPasswordConfirm("새로운 비밀번호 확인", false);

    private final String description;
    private final boolean optional;

    ChangePasswordField(String description, boolean optional) {
        this.description = description;
        this.optional = optional;
    }
}

테스트 작성시 아래와 같이 선언해주어야 한다.

private final RestDocsDescriptor changePasswordFieldHandler = new RestDocsDescriptor(ChangePasswordField.values());

위와 같이 하면 필드 정의는 끝났다.

테스트 작성

항목 정의 기능

  • RestDocsDescriptor.of(Descriptor... field) : 정의할 필드를 입력한다. 입력하지 않으면 전체를 필드를 정의한다.

최종 완료 기능

  • RestDocsDescriptor.exclude(Descriptor.... field) : payload 에 제외한 필드를 정의한다.
  • RestDocsDescriptor.stream() : java stream 을 반환한다. Stream
  • RestDocsDescriptor.collect(DescriptorCollectors::xxxDescriptor) : RestDocs 에서 사용할 수 있도록 리스트로 반환한다.
    • DescriptorCollectors::headerDescriptor
    • DescriptorCollectors::linkDescriptor
    • DescriptorCollectors::fieldDescriptor
    • DescriptorCollectors::requestPartDescriptor
    • DescriptorCollectors::parameterDescriptor

예시 코드

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureMvcRestDocs
class AuthenticatedAccountRestControllerTest {
    @Autowired
    private MockMvc mvc;

    @Autowired
    private ObjectMapper mapper;

    private final RestDocsDescriptor changePasswordFieldHandler = new RestDocsDescriptor(ChangePasswordField.values());

    private String pathPrefix;
    private String restdocsPath;

    @BeforeEach
    void init() {
        pathPrefix = "/v1/me";
        restdocsPath = "accounts/v1/me/{method-name}";
    }

    @Test
    void changePassword() throws Exception {

        AccountRequestDto.ChangePassword changePassword = AccountRequestDto.ChangePassword.builder()
            .currentPassword("1234")
            .newPassword("aaaa")
            .newPasswordConfirm("aaaa")
            .build();

        mvc.perform(patch(pathPrefix + "/password")
            .content(mapper.writeValueAsString(changePassword))
            .contentType(MediaType.APPLICATION_JSON)
        )
            .andExpect(status().isOk())

            .andDo(document(restdocsPath,
                requestHeaders(
                    headerWithName(HttpHeaders.CONTENT_TYPE).description(MediaType.APPLICATION_JSON)
                ),

                requestFields(
                    changePasswordFieldHandler.of(ChangePasswordField.currentPassword, ChangePasswordField.newPassword, ChangePasswordField.newPasswordConfirm)
                        .collect(DescriptorCollectors::fieldDescriptor)
                )
            ));
    }
}

그외 사용 예시 참고

추가적인 이슈

필드가 추가되거나 변경될 경우 모든 페이지에 수정작업이 발생된다.

하여 필드를 정의하는 enum 구현에서 정적 메서드로 명시적인 필드를 제공하는 것이 효율적일 수 있다.

코드 1-1 을 참고하여 아래와 같이 작성할 수 있다.

@Getter
public enum AccountField implements FieldSpec {
    ... skip ...

    public static String[] request() {
        return new String[]{
            AccountField.username,
            AccountField.password,
            AccountField.name,
            AccountField.disabled,
            AccountField.blocked
        };
    }

    public static String[] profile() {
        return new String[]{
            AccountField.uid,
            AccountField.username,
            AccountField.name,
            AccountField.registeredOn,
            AccountField.updatedOn
        };
    }
}