개발 낙서장

[TIL] 내일배움캠프 39일차 - 테스트 Validate 본문

Java/Sparta

[TIL] 내일배움캠프 39일차 - 테스트 Validate

권승준 2024. 2. 20. 21:11

 

 

오늘의 학습 키워드📚

테스트 환경에서 Validtate

DTO 테스트 코드를 작성할 때 DTO에 대한 검증은 어떻게 해야 할까?

@Getter
@Builder
public class SignUpRequestDto {
    @NotBlank(message = "사용자 이름이 공백이면 안됩니다.")
    @Size(min = 4, max = 10, message = "사용자 이름의 크기가 4에서 10 사이여야 합니다.")
    @Pattern(regexp = "^[a-z0-9]*$", message = "사용자 이름은 영어(소문자)랑 숫자만 가능합니다.")
    private String username;

    @NotBlank(message = "사용자 비밀번호가 공백이면 안됩니다.")
    @Size(min = 8, max = 15, message = "사용자 비밀번호의 크기가 8에서 15 사이여야 합니다.")
    @Pattern (regexp = "^[a-zA-Z0-9]*$", message = "사용자 비밀번호는 영어랑 숫자만 가능합니다.")
    private String password;

    private boolean isAdmin = false;

    private String adminToken = "";
}

회원가입 시 요청받는 SignUpRequestDto 클래스이다.
보다시피 유저 이름, 비밀번호에 이런저런 검증을 위한 어노테이션이 달려있다.

실제 서비스 환경에서는 Controller에 @Valid 어노테이션을 사용해 자동으로 검증이 가능하지만 테스트 환경에서는 어떻게 해야 될지 난감했다.
뭐 당연히 조건에 맞게 직접 검증 함수를 구현하는 것도 방법이겠지만 만약 Dto의 검증 조건이 바뀐다면? 그럴 때마다 테스트 환경에서의 검증 메소드도 같이 바꿔줘야 하는데 유지보수 측면에서 너무나도 비효율적이다.

Validator

그래서 jakarta에서 제공하는 Validator라는 인터페이스가 있다!

package jakarta.validation;

public interface Validator {
    <T> Set<ConstraintViolation<T>> validate(T var1, Class<?>... var2);

    <T> Set<ConstraintViolation<T>> validateProperty(T var1, String var2, Class<?>... var3);

    <T> Set<ConstraintViolation<T>> validateValue(Class<T> var1, String var2, Object var3, Class<?>... var4);

    BeanDescriptor getConstraintsForClass(Class<?> var1);

    <T> T unwrap(Class<T> var1);

    ExecutableValidator forExecutables();
}

여기서 validate라는 메소드를 통해 객체 검증이 가능하다.
(특정 프로퍼티, 특정 프로퍼티의 특정 값에 대해서도 검증이 가능하다. 사용해보지는 않았지만😐)

사용 방법은 ValidatorFactory라는 객체에서 Validator를 가져온 다음 검증하고 싶은 객체를 넣어 validate 하면 자동으로 검증이 돼 결과가 나온다.

    @BeforeAll
    static void beforeAll() {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    public <T> void validateAndCollectMessages(T requestDto) {
        Set<ConstraintViolation<T>> violations = validator.validate(requestDto);
        for (ConstraintViolation<T> violation : violations) {
            System.out.println(violation.getMessage());
            messages.add(violation.getMessage());
        }
    }

먼저 validator를 구성해준 후에 검증하고 싶은 객체를 받아 검증해 어떤 부분에서 오류가 발생했는지를 출력하는 메소드이다.
해당 메소드를 제네릭으로 만들고 DtoTest라는 클래스에 넣어 다른 클래스에서 DtoTest를 상속받아 유연하게 사용할 수 있도록 했다.

    @Test
    @DisplayName("유저 이름 공백 테스트")
    void 유저이름공백() {
        SignUpRequestDto requestDto = SignUpRequestDto.builder().username("").password("abcde123")
            .build();

        validateAndCollectMessages(requestDto);

        assertTrue(messages.contains(MSG_USERNAME_EMPTY));
        assertTrue(messages.contains(MSG_USERNAME_LENGTH));
    }

SignUpDtoTest 클래스에서 DtoTest 클래스를 상속받아 해당 메소드를 사용해 유저 이름이 공백인지 아닌지를 체크하는 테스트 메소드이다.

아주 성공적이다!

근데 해당 코드를 작성할 때 문제점이 하나 있었는데, @NotBlank와 @Size 등 검증에 대한 결괏값이 Dto에서 작성한 순서대로 나오는 것이 아니라 순서가 보장되지 않다는 것이었다.
그래서 처음에는 assertLinesMatch를 통해 String List를 만들어 검증을 시도했는데 두 순서가 달라져 때때로 실제로는 성공했지만 테스트는 실패하는 경우가 발생했다.
(예를 들어 {"유저 공백", "유저 길이"} 로 검증을 시도했는데 어떤 테스트에서는 {"유저 길이", "유저 공백"}, 어떤 테스트에서는 {"유저 공백", "유저 길이"} 이렇게 결과가 달라서 테스트 결과가 보장되지 않는 문제가 있었다.)

그래서 해결한 방법이 List에 contains를 통해 해당 문자열이 포함되는지를 체크했다.
너무 간단하게 해결할 수 있었는데 쓸데없이 되려 복잡하게 검증하려 했나 싶기도 하지만... 다음부터는 보장되지 않는 테스트를 진행하는 것은 지양해야겠다는 생각이 들었다.


오늘의 회고💬

기존 프로젝트에서 테스트 코드를 작성하는 과제가 시작됐다. DTO, Entity, Controller, Service, Repository에 대한 테스트를 진행하면 된다. 이전에 진행했던 과제들 보다는 생각보다 직관적이고 이해도 잘 돼서 재밌게 구현할 수 있던 것 같다.

 

내일의 계획📜

내일까지 우선 과제를 모두 끝내는 것을 목표로 진행해야지!

Comments