스프링 Junit5 회원가입 유효성 & 중복 테스트 작성
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은 공백을 제거해준다.
정리해가면서 열심히 공부해야겠다!
