본문 바로가기

개발/Java & Spring

[Java/Spring] javax.validation @Size vs @NotBlank과 Validation Test

아래와 같이 javax.validation 어노테이션을 사용한 Dto 를 만들다가 문득 @NotBlank (Null과 Blank 값을 검증하는)와 @Size 를 중첩할 필요가 있을까? 하는 생각이 들었다.

생각해보니 빈값과 null 값은 당연히 다르니까 둘 다 써야겠다라는 결론이 나긴 했는데 Dto에 붙은 Validation 은 어떻게 단위테스트를 짤까하다찾아보게 되었다

 

게시글은 아래를 참고했다! 

https://stackoverflow.com/questions/29069956/how-to-test-validation-annotations-of-a-class-using-junit

 

How to test validation annotations of a class using JUnit?

I need to test the validation annotations but it looks like they do not work. I am not sure if the JUnit is also correct. Currently, the test will be passed but as you can see the specified email a...

stackoverflow.com

 

package com.refactoring.fcm.board.dto;


import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ArticleInsertRequestDto {

    @NotBlank  // @Size 만으로 검증 가능하지 않을까?
    @Size(min = 1, max = 30, message = "제목이 너무 길거나 짧습니다!!")
    private String subject;

    @NotBlank
    @Size(min = 1, max = 10000000, message = "내용이 너무 길거나 짧습니다!!")
    private String content;

    @Builder(builderMethodName = "createBuilder")
    public ArticleInsertRequestDto(String subject, String content) {
        this.subject = subject;
        this.content = content;
    }
}

 

 

테스트 한 Dto는 위와 같고, 테스트코드는 아래와 같다.

package com.refactoring.fcm.board.dto;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

import static org.junit.Assert.assertTrue;

class ArticleInsertRequestDtoTest {

    private static Validator validator;

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

    @DisplayName("@Size @NotBlank")
    @Test
    void test() {
        ArticleInsertRequestDto articleInsertRequestDto = ArticleInsertRequestDto.createBuilder()
                .subject("") // @NotBlank, @Size에 모두 걸리는 상황
                .content("")
                .build();

        Set<ConstraintViolation<ArticleInsertRequestDto>> violations = validator.validate(articleInsertRequestDto);

        assertTrue(!violations.isEmpty()); // 비어있지 않음! -> 검증에 실패함
        
    }

}

 

 

javax.validation 에서 제공하는 DefaultValidatorFactory 에서 제공하는 validator 를 통해 검증하는 코드인데,

일단 위 테스트 코드는 통과한다. 즉, violations 에 검증을 실패한 내용이 들어간다.

 

violations 에 뭐가 들어있나 디버깅해보니 

 

 

@NotBlank 와 @Size 각각에 걸린 내용들이 들어가 있었다. 

 

살펴보면,

 

@NotBlank 의 디폴트 메시지, @Size 의 메시지가 출력된다. 

 

순서는 예상과 다르게 돌아간다. subject 필드 먼저 검사하고 content 검사할 줄 알았는데 .. 알파벳 순서인건가?

여기서 메시지 검증은 힘들지 않을까 생각했다

 

 

혹시나 @Size가 Null 체크도 해줄까 싶어  @NotBlank 를 주석처리하고 다음과 같이 테스트 해보았다.

 @DisplayName("@Size로 Null 값 검증 되나?")
    @Test
    void testnull() {
        ArticleInsertRequestDto articleInsertRequestDto = ArticleInsertRequestDto.createBuilder()
                .subject(null)
                .content(null)
                .build();

        Set<ConstraintViolation<ArticleInsertRequestDto>> violations = validator.validate(articleInsertRequestDto);

        assertTrue(!violations.isEmpty());

    }

 

 

아 안된다 ..! 

 

 

초반에 말했다시피 Size 가 Null 체크 안하는건 당연한건데 @NotBlank, @Size 가 더덕더덕 붙어있는 느낌이라 뭐라도 빼고 싶었던 것 같다 ㅋㅋㅋㅋ 

 

쨋든 에러메시지를 따로 정해두고 빈 값은 @Size 를 통해 검증하고 null 값은 @NotNull 로 검증해서 중복되지 않고 메시지를 따로 가져가는게 나을 것 같다고 자체적으로 결론을 내렸다

 

수정한 Dto 와 테스트는 아래와 같다.

package com.refactoring.fcm.board.dto;


import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ArticleInsertRequestDto {

    @NotNull(message = "subject에는 null이 들어올 수 없습니다") 
    @Size(min = 1, max = 30, message = "subject은 1자이상 30자 이하여야합니다.")
    private String subject;

    @NotNull(message = "content에는 null이 들어올 수 없습니다")
    @Size(min = 10,  message = "content는 10자 이상이어야 합니다")
    private String content;

    @Builder(builderMethodName = "createBuilder")
    public ArticleInsertRequestDto(String subject, String content) {
        this.subject = subject;
        this.content = content;
    }
}
@DisplayName("@Size @NotNull 테스트")
@Test
void testSizeNotNull() {
    ArticleInsertRequestDto articleInsertRequestDto = ArticleInsertRequestDto.createBuilder()
           .subject(null)
           .content("10자이하")
           .build();

    
    Set<ConstraintViolation<ArticleInsertRequestDto>> violations = validator.validate(articleInsertRequestDto);

    
    List<ConstraintViolation> constraintViolations = violations.stream()
        .collect(Collectors.toList());
    assertThat(violations.size()).isEqualTo(2); // subject의 NotNull, content 의 Size
    assertAll(
            () -> assertThat(constraintViolations.get(0).getMessage()).isEqualTo("subject에는 null이 들어올 수 없습니다"),
            () -> assertThat(constraintViolations.get(1).getMessage()).isEqualTo("content는 10자 이상이어야 합니다")
    );

}

 

아래 메시지 체크는 불확실하다고 보인다.

나는 여러번 실행해본 결과 순서는 늘 같아서 한번 디버그 해보고 저렇게 체크했지만, 디버깅 하기 전에는 뭐가 먼저 나올지 모른다.

 

 

보통 저렇게 Validation 을 실패하면 Exception 을 던져주니까 윗단에서 Exception 테스트를 해도 될 것 같긴한데, 갑자기 Dto 단테도 함 짜볼까 싶었다 ㅎㅅㅎ 

 

+ Java 게시글 너무 뜸하기도 했고 ,,