스프링

스프링 Junit5 회원가입 유효성 & 중복 테스트 작성

hissic 2025. 2. 20. 00:07

UserService의 단위테스트를 목적으로, 회원가입 유효성 검사와 중복 검사 기능 테스트에 대한 글이다.

Spring 3.x 버전이며 Junit5를 사용했다.

원래 Junit4를 사용하려 했는데, Junit5를 사용한 이유는 Junit5의 @ParameterizedTest 기능이 쓰고싶어서이다. 

 

 

- @ParameterizedTest란?

이 기능은 하나의 테스트를 여러 개의 입력 값으로 반복 실행할 수 있게 하는 어노테이션이다.

나의 경우 비밀번호 유효성 케이스가 6개 있었는데, Junit4 사용시 하나하나 테스트를 만들어줘야 했기에 과감히 Junit5를 사용해보기로했다. 

 

 


 

사전 준비 코드

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    private Validator validator;

    @InjectMocks
    private UserService userService; 

    @Mock
    private UserRepository userRepository;

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

 

- @ExtendWith(MockitoExtension.class)

Junit5에서 Mockito를 사용할 수 있도록 확장을 등록하는 어노테이션이다.

 

- Validator 

유효성 검사를 위한 것으로, Bean Validation API를 활용하기 위해 선언된 변수이다.

 

- @InjectMocks

테스트할 대상 객체에 @Mock으로 생성된 객체들을 자동으로 주입하는 역할을 한다.

  • UserService는 UserRepository를 필요로 하기 때문에, @Mock으로 선언된 UserRepository를 자동으로 주입받는다.

 

- @Mock

가짜 객체를 생성한다. 따라서 DB 접근 없이 테스트가 가능하다.

 

 

- @BeforeEach

테스트 실행 전에 수행된다.

  • setUp 메서드는 ValidatorFactory를 이용해 Validator를 초기화한다.
    이를 통해 매 테스트마다 새로운 Validator 객체가 생성되어 독립적인 테스트 환경을 유지할 수 있다.

 


회원가입 아이디 유효성 검사

    @Test
    public void 회원가입_아이디_유효성() {
        // Given: 아이디가 5자 미만
        UserSignUpDto userDto = new UserSignUpDto("abc", "Password123!", "nickname", "test@email.com");

        // When
        Set<ConstraintViolation<UserSignUpDto>> violations = validator.validate(userDto);
        BindingResult bindingResult = new BeanPropertyBindingResult(userDto, "userSignUpDto");

        for (ConstraintViolation<UserSignUpDto> violation : violations) {
            bindingResult.rejectValue(violation.getPropertyPath().toString(), 
            "valid_" + violation.getPropertyPath(), 
            violation.getMessage());
        }

        Map<String, String> errors = userService.validateHandling(bindingResult);

        // Then
        assertTrue(errors.containsKey("valid_username"), "valid_username 키가 존재하지 않습니다.");
        assertEquals("아이디는 5~20자 사이여야 합니다.", errors.get("valid_username"));
    }

 

- Given: 테스트 데이터 생성

아이디가 5자 미만인 유효하지 않은 값을 가진 회원가입 Dto를 생성한다.

 

- When: 유효성 검사 실행

Set<ConstraintViolation<UserSignUpDto>> violations = validator.validate(userDto);

  • validator.validate(userDto)를 실행하면, UserSignUpDto의 필드 값들이 유효성 검사(@pattern 등...)을 통과하는지 확인한다.
    유효성 검사를 통과하지 못한 항목들은 Set에 담긴다. 즉 오류가 없으면 빈 상태, 오류가 있으면 하나 이상의 ConstraintViolation 객체가 들어있다.

 

BindingResult bindingResult = new BeanPropertyBindingResult(userDto, "userSignUpDto");

  • userDto(검증할 객체)와 "userSignUpDto"(객체 이름)를 넣어서 bindingResult 객체를 생성한다.

 

rejectValue(field, errorCode, defaultMessage) 형식

  • field: 오류가 발생한 필드 이름 (violation.getPropertyPath().toString())
  • errorCode: 오류 코드 ("valid_" + violation.getPropertyPath(), 예: "valid_username")
  • defaultMessage: 오류 메시지 (violation.getMessage(), 예: "아이디는 5~20자 사이여야 합니다.")

 

userService.validateHandling(bindingResult)

  • 오류를 Map<String, String>으로 변환한다. (UserService의 유효성 검사 메소드)

 

→  최종적으로 오류 메시지가 "valid_username": "아이디는 5~20자 사이여야 합니다." 형태로 저장됨.

 

- Then: 검증

assertTrue

.containsKey()로 키가 존재하는지 확인한다.

assertEquals

value 값이 같은지 확인한다.

 

 


 

 

회원가입 아이디 중복 검사

    @Test
    public void 회원가입_중복_검사() throws Exception {
        // Given
        UserSignUpDto dto = new UserSignUpDto("duplicateUser", "Password1!", "uniqueNick", "duplicate@email.com");

        // Mocking: 중복
        when(userRepository.existsByUsername(dto.getUsername())).thenReturn(true);
        when(userRepository.existsByNickname(dto.getNickname())).thenReturn(true);
        when(userRepository.existsByEmail(dto.getEmail())).thenReturn(true);

        // When
        Map<String, String> errors = userService.checkDuplication(dto);

        // Then
        assertFalse(errors.isEmpty()); // 중복된 필드가 있어야 함
        System.out.println("Errors: " + errors);
        assertEquals("이미 존재하는 아이디입니다.", errors.get("Duplication_username"));
        assertEquals("이미 존재하는 이메일입니다.", errors.get("Duplication_email"));

        // Verify: mock이 호출되었는지 확인
        verify(userRepository, times(1)).existsByUsername(dto.getUsername());
        verify(userRepository, times(1)).existsByNickname(dto.getNickname());
        verify(userRepository, times(1)).existsByEmail(dto.getEmail());
    }

- Given: 테스트 데이터 생성

 

  • userRepository.existsBy...() 메서드가 DB에서 중복 검사하는 역할을 한다.
  • when(...).thenReturn(...)을 사용하여 DB에 있는 것처럼 동작하도록 설정한다.
    ex) existsByUsername( dto.getUsername() ) → true (중복된 아이디)

- When: 중복 검사 실행

userService.checkDuplication(dto)을 통해 오류 메세지를 Map<String, String> 형태로 저장한다.

 

- Then: 검증

assertFalse

중복된 필드가 있기 때문에 비어있으면 안된다. (=False)

assertEquals

value 값이 같은지 확인한다.

 

- Verify

existsBy... 메서드가 한 번씩 호출되었는지 검증한다.

 

 


회원가입 비밀번호 유효성 검사

    static Stream<Arguments> invalidPasswords() {
        String errorMessage = "비밀번호는 8~16자이며, 영문 대 소문자, 숫자, 특수문자를 사용하세요.";

        return Stream.of(
                Arguments.of("Abc1@", errorMessage),  // 8자 미만
                Arguments.of("VeryLongPassword123!", errorMessage), // 16자 초과
                Arguments.of("NoNumber!", errorMessage), // 숫자 없음
                Arguments.of("NoSpecial123", errorMessage), // 특수문자 없음
                Arguments.of("FLOWER123!", errorMessage), // 소문자 없음
                Arguments.of("flower123!", errorMessage) // 대문자 없음
        );
    }

    @DisplayName("비밀번호 유효성 검사 테스트")
    @ParameterizedTest
    @MethodSource("invalidPasswords")
    public void 회원가입_비밀번호_유효성(String password, String expectedMessage) {
        // Given
        UserSignUpDto userDto = new UserSignUpDto("validUser", password, "nickname", "test@email.com");

        // When
        Set<ConstraintViolation<UserSignUpDto>> violations = validator.validate(userDto);
        BindingResult bindingResult = new BeanPropertyBindingResult(userDto, "userSignUpDto");

        for (ConstraintViolation<UserSignUpDto> violation : violations) {
            String fieldName = violation.getPropertyPath().toString();
            bindingResult.rejectValue(fieldName, "valid_" + fieldName, violation.getMessage());
        }

        Map<String, String> errors = userService.validateHandling(bindingResult);

        // Then
        assertTrue(errors.containsKey("valid_password"), "valid_password 키가 존재하지 않습니다.");
        assertEquals(expectedMessage.trim(), errors.get("valid_password").trim());
    }

 

 

invalidPasswords() (테스트 데이터 제공 메서드)

Arguments.of(비밀번호, 기대 오류 메시지) 형식으로 여러 개의 테스트 데이터를 만든다.

 

회원가입_비밀번호_유효성

@ParameterizedTest
여러 개의 입력값을 사용해 반복 테스트를 수행한다.

 

    @CsvSource(value = {
            "Abc1@ | 비밀번호는 8~16자이며, 영문 대 소문자, 숫자, 특수문자를 사용하세요.",  // 8자 미만
            "VeryLongPassword123! | 비밀번호는 8~16자이며, 영문 대 소문자, 숫자, 특수문자를 사용하세요.", // 16자 초과
            "NoNumber! | 비밀번호는 8~16자이며, 영문 대 소문자, 숫자, 특수문자를 사용하세요.", // 숫자 없음
            "NoSpecial123 | 비밀번호는 8~16자이며, 영문 대 소문자, 숫자, 특수문자를 사용하세요.", // 특수문자 없음
            "ALLLOWER123! | 비밀번호는 8~16자이며, 영문 대 소문자, 숫자, 특수문자를 사용하세요." // 대문자 없음
    }, delimiter = '|')

 

원래 이렇게 | 를 구별문자로 해서 @CsvSource를 사용하려 했는데, 경고 문자가 같으면 중복을 줄일 수 있는 방법이 있지 않을까 싶었다. 그렇게 @MethodSource를 알게 되었다! 

 

@MethodSource("invalidPasswords")
앞에서 정의한 invalidPasswords() 메서드의 데이터를 테스트에 사용한다.

invalidPasswords() 메서드에서 매개변수에 값을 전달할 때, Arguments.of(값1, 값2) 형식으로 값1은 첫 번째 매개변수에, 값2는 두 번째 매개변수에 자동으로 매핑된다. (String password, String expectedMessage)

 

 

- Given: 테스트 데이터 생성

- When: 유효성 검사 실행

아이디 유효성 검사와 비슷하므로 생략하겠다.

- Then: 검증

.trim은 공백을 제거해준다.

 

 


 

 

정리해가면서 열심히 공부해야겠다!