restdocs에서 제약조건(Constraint) 관련 정보가 나오도록 커스텀할 수 있습니다.

 

import org.springframework.restdocs.constraints.ConstraintDescriptions

public void example() {
    ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); 
    List<String> descriptions = userConstraints.descriptionsForProperty("name"); 
}

static class UserInput {
    @NotNull
    @Size(min = 1)
    String name;

    @NotNull
    @Size(min = 8)
    String password;
}

ConstraintDescriptions 클래스를 이용해 해당 java 클래스의 constraint description 정보를 불러오고, map처럼 property 이름으로 constraint 정보들을 갖고올 수 있습니다.

 

|===
|Path|Type|Description|Constraints

{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{#constraints}}{{.}}{{/constraints}}{{/tableCellContent}}

{{/fields}}
|===

request-fields.snippet 파일은 이런식으로 작성하면 되고, src/test/resources/org/springframework/restdocs/templates/request-fields.snippet 이 경로에 넣어야 합니다.

 

예제 (번거로운 버전)

package com.vince.interfaces.user

import kotlinx.serialization.Serializable
import javax.validation.constraints.NotEmpty
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

@Serializable
data class SignupRequest(
    @field:NotEmpty
    val username: String,

    @field:NotEmpty
    @field:Size(min = 8)
    val password: String,

    @field:Pattern(regexp = "010\\d{7,8}")
    val phoneNumber: String,

    @field:NotEmpty
    val name: String,
)

Request dto를 이렇게 만들었다고 했을 때, 이 제약정보를 일일이 description에 넣는건 매우 번거롭고 유지보수가 안돼 restdocs와 실제 constraint 사이 간극이 생기기 쉽습니다

따라서 restdocs에서 제공하는 ConstraintDescriptions 클래스를 사용하면 됩니다

 

@Test
@DisplayName("회원가입 테스트")
fun signUp() {
    val signupRequest = SignupRequest(
        "username",
        "password",
        "01012341234",
        "홍길동",
    )
    val constraints = ConstraintDescriptions(SignupRequest::class.java)
    
    // 회원가입 테스트 코드
    // ...

    mockMvc.perform(
        post("/user/signup")
            .contentType(MediaType.APPLICATION_JSON)
            .content(Json.encodeToString(signupRequest))
    )
        .andExpect(status().isOk)
        .andDo(
            document(
                "users/signup",
                requestFields(
                    fieldWithPath("username").description("유저명 (아이디)"),
                    fieldWithPath("password").description("패스워드")
                        .attributes(key("constraints").value(
                            constraints.descriptionsForProperty("password").joinToString("\n")
                        )),
                    fieldWithPath("phoneNumber").description("핸드폰 번호")
                        .attributes(key("constraints").value(
                            constraints.descriptionsForProperty("phoneNumber").joinToString("\n")
                        )),
                    fieldWithPath("name").description("이름"),
                ),
            )
        )
}
Path / Type / Description / Constraints
phoneNumber / String / 핸드폰 번호 / Must match the regular expression 010\d{7,8}

대략 코드는 이렇고, 출력은 위처럼 나옵니다

문제는 똑같은 코드가 반복되고 있다는건데 너무 번거롭습니다.

 

개선된 버전

package com.vince.helper

import org.springframework.restdocs.constraints.ConstraintDescriptions
import org.springframework.restdocs.payload.FieldDescriptor
import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import org.springframework.restdocs.snippet.Attributes.key

class ConstrainedFields(clazz: Class<*>) {
    private val constraintDescriptions = ConstraintDescriptions(clazz)

    fun withPath(path: String): FieldDescriptor = fieldWithPath(path).attributes(
        key("constraints").value(
            this.constraintDescriptions.descriptionsForProperty(path)
                .joinToString(separator = "\n\n") { "- $it" }
        )
    )
}

헬퍼 클래스를 테스트 모듈 안에 추가해주시고

 

@Test
@DisplayName("회원가입 테스트")
fun signUp() {
    val signupRequest = SignupRequest(
        "username",
        "password",
        "01012341234",
        "홍길동",
    )

    val fields = ConstrainedFields(SignupRequest::class.java)  // (1)
    
    // 회원가입 테스트 코드
    // ...

    mockMvc.perform(
        post("/user/signup")
            .contentType(MediaType.APPLICATION_JSON)
            .content(Json.encodeToString(signupRequest))
    )
        .andExpect(status().isOk)
        .andDo(
            document(
                "users/signup",
                requestFields(
                    // (2)
                    fields.withPath("username").description("유저명 (아이디)"),
                    fields.withPath("password").description("패스워드"),
                    fields.withPath("phoneNumber").description("핸드폰 번호"),
                    fields.withPath("name").description("이름"),
                ),
            )
        )
}

이런식으로 쓰면 됩니다. 더이상 constraint attribute를 손수 넣지 않아도 됩니다!

 

결과물은 이런식으로 나옵니다.

잘 설정하면 locale을 넣을 수도 있을 것 같은데.. 이건 좀 찾아봐야겠네요

 

참고

반응형