JPA 게시판 프로젝트 - 테스트 코드 작성하기(User Validation)

DevSeoRex·2023년 1월 15일
0
post-thumbnail

기본적인 회원 기능에 대한 테스트 코드까지 작성을 완료 했었습니다.
이제는 회원을 가입 시키게 되면 잘못된 값들이 들어왔을때 검증을 통해 어떤 부분이 잘못 입력되었는지 사용자에게 알려주어야 합니다.
그렇기 때문에 유효성 검사를 통해 사용자에게 에러 메시지를 보여주어야 하는데요.

Validation을 통해 에러 메시지를 사용자에게 보여주려고 하는 이유는 톰캣에서 보여주는 다소 적나라한(?) 에러 메시지를 사용자에게 보여주게 되면 톰캣 버전과 같은 민감한 정보들이 사용자에게 표시 되므로, 공격의 표적이 될 수 있기 때문입니다.


이러면 안되기 때문에! 사용자가 잘못된 입력을 주었을때 에러를 바인딩해서 사용자의 입력의 어떤 부분에 문제가 생겼는지 알려주기 위해 Validation을 하게 됩니다. 흔히 유효성 검사라고 부르기도 합니다.

😲 어떻게 테스트 해야 할까요?

오늘 테스트해야 하는 내용은 User의 회원 가입시 값을 바인딩하는 클래스인 UserRequestDto 클래스에 유효성 검사를 할 수 있도록 애너테이션을 사용해 작성해두었습니다.

일단 클래스를 먼저 살펴보겠습니다.

// UserRequestDto 클래스
@Getter @Setter
@AllArgsConstructor @Builder
@NoArgsConstructor
public class UserRequestDto {

    private Long no;

    @NotEmpty(message = "회원 ID는 필수 입력 항목입니다.")
    @Length(max = 30, message = "회원 ID는 최대 30자까지 입력 가능합니다.")
    private String id;

    @NotEmpty(message = "회원 이름은 필수 입력 항목입니다.")
    @Length(max = 20, message = "회원 이름은 최대 20자까지 입력 가능합니다.")
    private String name;

    @NotEmpty(message = "비밀번호는 필수 입력 항목입니다.")
    @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*\\W).{8,20}$", message = "비밀번호는 영문과 특수문자를 포함하며 8자 이상이어야 합니다.")
    private String pwd;

    @NotEmpty(message = "이메일은 필수 입력 항목입니다.")
    @Email(message = "이메일 형식에 맞게 입력해주세요.")
    private String email;



    public User userRequestToEntity() {
        return User.builder()
                .no(no)
                .id(id)
                .name(name)
                .pwd(pwd)
                .email(email)
                .build();
    }
}

UserRequestDto는 User 클래스(Entity)가 가지고 있는 모든 필드를 가지고 있습니다. User 클래스와 다른 점은 User 클래스에는 Validation을 위한 애너테이션이 붙어있지 않기 때문입니다.

Entity에서 Validation을 하게 되면, Entity가 Validation을 위한 애너테이션이나 로직으로 인해 오염되기 때문에 엔티티는 엔티티 그 자체로 사용하고, 화면에서 필요한 필드나 관련 로직을 가진 DTO를 만들어 사용해야 합니다.

사용된 애너테이션들을 간단히 설명드리면 아래와 같습니다.

애너테이션 이름기능
@NotEmpty필드의 값이 비어있는지 검사합니다.
@Length필드의 길이의 최소, 최대 값을 제한할 수 있습니다.
@Pattern필드가 작성한 정규표현식에 위배되는지 검사합니다.
@Email필드가 이메일 형식에 맞는지 검사합니다.

message에 작성한 내용들은 유효성 검사에 위배되어 사용자에게 노출될 메세지를 작성하는 곳입니다.

지금부터는 유효성 검사 기능이 제대로 작동하는지 확인하기위해 테스트 코드를 작성하겠습니다.

😈 회원가입 유효성 검사 테스트 코드 작성

일단 기본적인 설정에 대해서 작성해보겠습니다.

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserApiTest {

    @Autowired private Validator validator;
 }

Spring과 통합환경에서 테스트를 진행해야 하기 때문에, 오늘도 어김없이 @SpringBootTest 애너테이션과 @RunWith(SpringRunner.class)를 클래스 위에 붙여줍니다.

또 Validation을 하기 위해서 사용해야 하는 Validator를 주입 해줍니다.
이렇게 테스트 코드를 작성해 테스트할 환경은 모두 구성하였습니다.

저는 회원 유효성 검사를 좀 더 단순하고 간단하게 하기 위해서, Hibernate-validator를 사용하였습니다.

🐶 필수항목 유효성 검사 - @NotEmpty 애너테이션

@NotEmpty 애너테이션은 필드의 값이 비어 있는지 여부를 검사하게 됩니다.
값이 비어있어, 유효성 검사에 실패하게 되면, @NotEmpty 애너테이션을 붙일때 작성한 message가 사용자에게 노출되게 됩니다.

	@Test
    @DisplayName("필수항목 유효성 검사 테스트")
    public void validateUserIsRequired() {
        // given
        UserRequestDto user = UserRequestDto.builder()
                .build();

        // when
        Set<ConstraintViolation<UserRequestDto>> validate = validator.validate(user);

        // then
        Iterator<ConstraintViolation<UserRequestDto>> iterator = validate.iterator();
        List<String> messageList = new ArrayList<>();

        while(iterator.hasNext()) {
            String message = iterator.next().getMessage();
            messageList.add(message);
            System.out.println("message = " + message);
        }

        assertThat(messageList).contains("회원 이름은 필수 입력 항목입니다.",
                "회원 ID는 필수 입력 항목입니다.","비밀번호는 필수 입력 항목입니다.",
                "이메일은 필수 입력 항목입니다.");
    }

회원이 필수 항목에 모두 값을 입력했는지 검증하는 테스트 시나리오는 아래와 같습니다.

  1. 유효성 검사를 위해, UserReuestDto에 모든 필드를 초기화 하지 않고, UserRequestDto의 builder를 이용하여 객체를 생성합니다.

  2. 주입받은 validator의 validate 메서드를 통해 유효성 검사의 결과를 반환 받습니다.

  3. Iterator에 유효성 검사의 결과를 저장해줍니다.

  4. Iterator로 유효성 검사의 결과들을 반복해서 순회하면서, 에러 메세지들을 messageList에 저장해줍니다.

  5. UserRequestDto에 정의한 메시지가 messageList안에 저장되어 있는지 검증합니다.

UserRequestDto에는 필드의 값이 비어 있을경우 보여줘야 할 메시지가 정의 되어 있습니다.

  • 이메일이 비어있을경우 : "이메일은 필수 입력 항목입니다."
  • 회원 이름이 비어있을경우 : "회원 이름은 필수 입력 항목입니다."
  • 비밀번호가 비어있을경우 : "비밀번호는 필수 입력 항목입니다."
  • 회원 ID가 비어있을경우 : "회원 ID는 필수 입력 항목입니다."

현재 UserRequestDto의 모든 필드가 비어 있으므로, validator의 메서드 validate를 통해 유효성 검사를 하면, 필드의 값이 비어 있을경우 보여줘야 할 메시지 전부가 반환되어야 합니다.

테스트 코드를 실행시켜 보도록 하겠습니다.

에러 메시지들이 출력된 것을 볼 수 있습니다.
UserRequestDto의 필드가 전부 비어 있기 때문에 모든 에러 메시지가 콘솔에 출력되었습니다.

그렇다면 테스트는 제대로 동작하고 있지만, 한번 더 검증을 해보도록 하겠습니다.
위의 코드와 다르게 UserRequestDto를 생성하는 과정에서 회원 이름 필드에 값을 주면 테스트가 실패하는지 성공하는지 확인해보겠습니다.

// UserRequestDto의 객체 생성시 회원의 이름에 값을 주도록 변경

// given
UserRequestDto user = UserRequestDto.builder()
                .name("RexSeo")
                .build();

회원의 이름이 입력 되었기 때문에, 테스트를 작성할때의 시나리오인 "회원의 모든 필드가 비어있다"를 만족하지 못해서 테스트가 실패하였고, 에러 메시지 또한 3개만 출력되는 것을 볼 수 있습니다.

따라서 지금 작성한 테스트 코드가 제대로 동작하고 있다는 것을 알 수 있습니다.

🤔 이메일 & 비밀번호 유효성 검사 - @Pattern & @Email 애너테이션

이메일과 비밀번호 유효성 검사는 두가지 애너테이션을 사용하여 진행하게 됩니다.
@Pattern과 @Email 애너테이션인데요.

@Pattern은 정규표현식을 값으로 받습니다. 값으로 입력한 정규표현식을 해당 필드가 위반하게 되면, 에러 메시지를 반환하게 됩니다.

@Email은 정의된 이메일 정규표현식을 위반하게 되면, 에러 메시지를 반환하게 됩니다.

@Pattern과 @Email의 차이점은, @Email은 이미 애너테이션을 만든 개발자가 정의한 패턴대로 유효성 검사를 진행한다는 것이고, @Pattern은 직접 정규 표현식을 입력해서 유효성 검사를 진행한다는 것입니다.

테스트 코드를 작성해 보겠습니다.

	@Test
    @DisplayName("비밀번호 & 이메일 형식 유효성 검사 테스트")
    public void validateUserIsHasPattern() {
        // given
        UserRequestDto user = UserRequestDto.builder()
                .id("Java")
                .name("Dev")
                .pwd("cadksfj")
                .email("cccaaaaa")
                .build();

        // when
        Set<ConstraintViolation<UserRequestDto>> validate = validator.validate(user);

        // then
        Iterator<ConstraintViolation<UserRequestDto>> iterator = validate.iterator();
        List<String> messageList = new ArrayList<>();

        while(iterator.hasNext()) {
            String message = iterator.next().getMessage();
            messageList.add(message);
            System.out.println("message = " + message);
        }

        assertThat(messageList).contains("이메일 형식에 맞게 입력해주세요.", "비밀번호는 영문과 특수문자를 포함하며 8자 이상, 20자 이하여야 합니다.");
    }

이번 테스트는 비밀번호와 이메일의 패턴(유효성)을 검사하는 것이므로, 필수 입력항목을 입력하지 않아 발생하는 에러 메시지가 나오지 않도록, 아이디와 이름 필드에 값을 주었습니다.

비밀번호는 영문과 특수문자를 포함하여 8자 이상, 20자 이하여야 하는 규칙이 있습니다.
이메일은 @Email 애너테이션 내부에 정의된 규칙을 따릅니다. 보통 이메일이라면 보통 aaa@aaa.com과 같은 아이디@메일 서비스 도메인과 같은 규칙을 가지고 있을 것입니다.

테스트 코드를 실행하면 어떤 프로세스로 동작하는지는 위에 작성된 필수 입력항목 검사 부분과 동일하기 때문에 따로 설명 드리지 않겠습니다.


테스트가 성공적으로 동작한 것을 볼 수 있습니다.

그렇다면, 이메일을 정상적인 패턴을 가지도록 값을 바꾸고 테스트를 진행해보겠습니다.

// 이메일 필드의 값을 정상적인 패턴으로 입력
// UserRequestDto
// given
UserRequestDto user = UserRequestDto.builder()
                .id("Java")
                .name("Dev")
                .pwd("cadksfj")
                .email("aaa@aaa.com")
                .build();


이메일을 정상적인 패턴인 aaa@aaa.com과 같이 제대로 입력하였을때 이메일과 비밀번호의 패턴이 유효성 검사에 위배된다는 시나리오로 테스트를 작성하였기 때문에, 테스트가 실패하는 것은 테스트가 정상적으로 동작했다고 볼 수 있겠습니다.

이번 테스트에 약간 이슈가 있는 부분이 있습니다.
@Email 애너테이션이 문제가 되는 패턴을 전부 잡아내지 못한다는 부분인데요, 이 부분은 나중에 따로 포스팅을 하도록 하겠습니다.

😼 회원 ID & 이름 유효성 검사 - @Length 애너테이션

오늘의 마지막 테스트 코드 파트입니다.
이번 테스트는 필드의 길이를 검사하는 @Length 애너테이션을 사용하는 테스트 코드를 작성하였습니다.
회원 ID는 30자 회원의 이름은 20자 이하로 작성 되어야 한다는 규칙을 주었습니다.
필드가 이 규칙을 어겼을 경우에 올바른 에러 메시지가 바인딩 되는지 확인하는 테스트 코드는
아래와 같습니다.

	@Test
    @DisplayName("회원 길이제한 유효성 검사 테스트")
    public void validateUserLengthTest() {
        // given
        UserRequestDto user = UserRequestDto.builder()
                .id("aaaaaaaaaaaaaaaajhdfkadshfjkdashflkashdfjkahsfjkdashfjkdashfjkdshaflajskdhfjkalsd")
                .name("Devaklsdfjlkdsajflsdfgsdfgfkdjsglfsdkjgklsgsgdf")
                .pwd("fdkjlk1232!")
                .email("ccc@ccc.com")
                .build();

        // when
        Set<ConstraintViolation<UserRequestDto>> validate = validator.validate(user);

        // then
        Iterator<ConstraintViolation<UserRequestDto>> iterator = validate.iterator();
        List<String> messageList = new ArrayList<>();

        while(iterator.hasNext()) {
            String message = iterator.next().getMessage();
            messageList.add(message);
            System.out.println("message = " + message);
        }

        assertThat(messageList).contains("회원 ID는 최대 30자까지 입력 가능합니다.", "회원 이름은 최대 20자까지 입력 가능합니다.");
    }

아이디와 이름의 길이를 체크하는 것이 테스트의 목적이므로, 이메일과 비밀번호 패턴 유효성 검사에서 에러를 바인딩 하지 않도록, 이메일과 비밀번호에는 올바른 값을 주었습니다.

아이디와 이름에는 아무 문자나 30자가 넘는 값을 주어서, UserRequestDto의 객체를 생성하였습니다.
테스트를 실행해보도록 하겠습니다.


테스트가 기대한대로 잘 수행된것을 볼 수 있습니다.

여기서 마지막으로 회원 ID를 30자 이하로 제대로 된 값을 셋팅해 보겠습니다.

// 회원 ID를 30자 이하로 셋팅
// given
UserRequestDto user = UserRequestDto.builder()
                .id("aaaaaaaaaaa")
                .name("Devaklsdfjlkdsajflsdfgsdfgfkdjsglfsdkjgklsgsgdf")
                .pwd("fdkjlk1232!")
                .email("ccc@ccc.com")
                .build();

테스트를 실행해보겠습니다.

회원 이름을 제대로 된 값으로 셋팅했기 때문에, 기대하던 에러 메시지들이 전부 메시지 List에 포함되어 있지 않아서 테스트가 실패한 것을 볼 수 있습니다.

테스트 코드가 모두 정상적으로 동작하는 것을 확인 했습니다.

🤖 Next Step..

원래 회원 유효성 검사를 할때 어떻게 테스트 코드를 작성할지 많이 고민을 했었습니다.

Mockito를 이용해서 Controller 테스트로 진행해야 할까? 아니면 다른 방법이 있을까?
현재 View를 만들지 않은 상태에서 백엔드 개발을 먼저 진행하고 있기 때문에 Mockito를 사용하기에는 조금 부적절하다고 생각했습니다.

그러던 와중 구글링을 하다보니, Validator 클래스를 사용해서 충분히 외부 라이브러리 없이 테스트를 할 수 있다는 정보를 알게 되어 직접 테스트 코드를 작성해보고 여러가지 테스트를 해보았습니다.

테스트 코드를 작성하면서 느낀점은 "검증을 하려면, 눈으로 확인할 수 있는 테스트 시나리오 작성을 통해 내 코드를 검증하자" 이번에 게시판을 느릿 느릿 만드는 프로젝트를 진행하며 느낀점은 바쁘다는 이유로, 시간이 없다고 미뤘던 테스트 코드 작성을 통해 TDD가 무엇인지 조금이라도 알아가고 있다는 것입니다.

앞으로도 어떤 사이드 프로젝트를 하더라도 꼭 테스트 코드를 짜는 그런 사람이 되어야겠다는 생각을 하며 마칩니다.

오늘도 제 벨로그에 와주신 분들께 감사 인사를 전하며, 2023년 좋은 일들만 가득하시길 바랍니다.

감사합니다.

2개의 댓글

comment-user-thumbnail
2023년 1월 15일

무지성 1빠

답글 달기