개발이 어느 정도 마무리 되고, 명세대로 테스트 코드를 작성하게 되었다.
지금까지 개발 중 테스트는 Postman으로 보내고, DB를 확인하는 방법으로 진행했었다.
테스트 기반 개발도 해보고 싶었는데, 이번에 테스트에 대해서 알아보고 다음 개발때는 한번 해봐도 좋을 것 같다.
일반적으로 단위 테스트, 통합 테스트, 종단 간 테스트로 나눌 수 있다.
1. 단위 테스트 (Unit Test)
@SpringBootTest 를 사용해 전체 애플리케이션 컨텍스트를 로드하고 테스트할 수 있음각 단계(단위 테스트, 통합 테스트, 종단 간 테스트)별로 검증해야 할 범위와 방법이 다름
@Test 어노테이션을 이용해 테스트 메소드를 정의@SpringBootTest 어노테이션을 사용해 애플리케이션 컨텍스트를 로드하여 통합 테스트 수행 가능종단 간 테스트는 계속 해왔던 것이고, Junit과 Mockito를 사용해서 단위 테스트를 진행해 볼 것이다
@Test: 테스트 메소드를 표시함. 이 어노테이션이 붙은 메소드는 테스트로 인식되며 실행됨.@BeforeEach: 각 테스트 메소드가 실행되기 전에 수행할 초기화 코드가 있는 메소드를 정의할 때 사용함. 테스트의 독립성을 보장하기 위해 사용됨.@AfterEach: 각 테스트가 끝난 후 실행되는 메소드를 정의함. 주로 리소스 정리 작업을 수행할 때 사용됨.@BeforeAll: 모든 테스트가 실행되기 전에 한 번만 수행될 설정 작업을 정의함.@AfterAll: 모든 테스트가 끝난 후 한 번만 실행될 작업을 정의함.@Disabled: 특정 테스트 메소드를 비활성화할 때 사용함.@Mock
private UserRepository userRepository;
// 또는 아래 방법 사용
UserRepository userRepository = Mockito.mock(UserRepository.class);
when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(new User()));
verify(userRepository).save(any(User.class));
verify(userRepository, times(1)).save(any(User.class)); // 호출 횟수도 검증 가능
when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(new User()));
verify(userRepository).findByEmail(anyString());
doThrow(new RuntimeException()).when(userRepository).delete(any(User.class));
1.의존성 분리
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.0'

이렇게 알아서 만들어준다.

public ResponseDto createUser(CreateUserRequestDto request) {
Point latLngPoint = geometryFactory.createPoint(new Coordinate(request.getLng(), request.getLat()));
String socialProvider = "NONE";
String password = request.getPassword();
// 이메일 중복확인
if(userRepository.findByEmail(request.getEmail()).isPresent()) {
return new ResponseDto<>(-1, "중복된 이메일입니다", null);
}
// 닉네임 중복확인
if(userRepository.findByNickname(request.getNickname()).isPresent()) {
return new ResponseDto<>(-1, "중복된 닉네임입니다", null);
}
// 카카오 회원가입일 경우
if(request.getKakaoId() != null) {
socialProvider = "KAKAO";
// 비밀번호 랜덤 생성
password = UUID.randomUUID().toString();
}
// 일반 회원가입인데 비밀번호가 없는 경우
if(request.getKakaoId() == null && password == null) {
return new ResponseDto<>(-1, "비밀번호가 없습니다", null);
}
// 비밀번호 암호화
String encodedPassword = passwordEncoder.encode(password);
P_user user = P_user.builder()
.email(request.getEmail())
.password(encodedPassword)
.nickname(request.getNickname())
.phone(request.getPhone())
.birth(request.getBirth())
.use_yn(true)
.role(UserRoleEnum.CUSTOMER)
.imageProfile(request.getImageProfile())
.latLng(latLngPoint)
.address(request.getAddress())
.kakaoId(request.getKakaoId())
.socialProvider(socialProvider)
.build();
userRepository.save(user);
return new ResponseDto<>(1, "회원가입이 완료되었습니다", null);
}
이 기능에 대해 테스트해야 할 주요 시나리오는 다음과 같다.
1. 이메일 중복
2. 닉네임 중복
3. 카카오 회원가입: 카카오 ID가 있는 경우 랜덤 비밀번호가 생성되는지 확인
4. 비밀번호 없음: 일반 회원인데 비밀번호가 없는 경우
5. 정상적인 회원 가입: 모든 필드가 유효한 경우
테스트를 위해 userRepository, passwordEncoder, geometryFactory 등을 Mocking하여 사용한다.
이메일 중복 상황 테스트
@Test
void createUserTest_EmailDuplicate() {
// Given
CreateUserRequestDto request = CreateUserRequestDto.builder()
.email("test@test.com")
.password("password1234")
.nickname("nickname")
.phone("phone")
.birth(LocalDate.parse("2000-01-01"))
.imageProfile("imageProfileUrl")
.lat(12.123124f)
.lng(123.123124f)
.address("상세주소")
.kakaoId(null)
.build();
when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.of(new P_user()));
[;''
// When
ResponseDto response = userService.createUser(request);
// Then
assertEquals(-1, response.getCode());
assertEquals("중복된 이메일입니다", response.getMsg());
}
이런 식으로 테스트 하면 된다.
닉네임 중복 테스트
@Test
void createUserTest_NicknameDuplicate() {
// Given
CreateUserRequestDto request = CreateUserRequestDto.builder()
.email("test@test.com")
.password("password1234")
.nickname("nickname")
.phone("010-1234-5678")
.birth(LocalDate.parse("2000-01-01"))
.imageProfile("imageProfileUrl")
.lat(12.123124f)
.lng(123.123124f)
.address("상세주소")
.kakaoId(null)
.build();
// Mocking: 이메일은 중복되지 않음
when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty());
// Mocking: 닉네임이 중복되는 상황 시뮬레이션
when(userRepository.findByNickname(request.getNickname())).thenReturn(Optional.of(new P_user()));
// When
ResponseDto response = userService.createUser(request);
// Then
assertEquals(-1, response.getCode());
assertEquals("중복된 닉네임입니다", response.getMsg());
}
카카오 회원가입 성공
@Test
void createUserTest_KakaoSignupSuccess() {
// Given
CreateUserRequestDto request = CreateUserRequestDto.builder()
.email("test@test.com")
.password(null) // 비밀번호는 랜덤으로 생성
.nickname("kakaoUser")
.phone("010-5678-1234")
.birth(LocalDate.parse("1998-03-17"))
.imageProfile("kakaoProfile.png")
.lat(37.5665f)
.lng(126.9780f)
.address("상세주소")
.kakaoId(123456789L) // 카카오 ID 설정
.build();
// Mocking: 이메일과 닉네임 중복 없음
when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty());
when(userRepository.findByNickname(request.getNickname())).thenReturn(Optional.empty());
// Mocking: 비밀번호 암호화
String randomPassword = UUID.randomUUID().toString(); // 생성된 랜덤 비밀번호
String encodedPassword = "encodedRandomPassword"; // 암호화된 비밀번호
when(passwordEncoder.encode(randomPassword)).thenReturn(encodedPassword);
// When
ResponseDto response = userService.createUser(request);
// Then
assertEquals(1, response.getCode());
assertEquals("회원가입이 완료되었습니다", response.getMsg());
// Verify: passwordEncoder.encode(...) 호출 확인
verify(passwordEncoder).encode(randomPassword);
// Verify: 저장된 사용자 정보 확인
verify(userRepository).save(argThat(user ->
user.getEmail().equals("kakao@test.com") &&
user.getNickname().equals("kakaoUser") &&
user.getKakaoId().equals(123456789L) &&
user.getSocialProvider().equals("KAKAO") &&
user.getPassword().equals(encodedPassword) // 암호화된 비밀번호 확인
));
}

생각없이 쓰다가 전달 객체랑 검증 객체 이메일을 다르게 써서 에러 발생.
잘 동작하는지 확인한거임 아무튼 그럼.

인코딩된 비밀번호 때문에 테스트가 실패했다.
randomPassword을 명시적으로 정의했지만, 서비스 로직에서 새로운 UUID를 생성하여 사용하고 있음when(passwordEncoder.encode())은 테스트 코드의 randomPassword와 연결되어 있지만 서비스 로직에서는 다른 UUID 값을 passwordEncoder.encode()에 전달함passwordEncoder.encode()가 호출될 때 당연히 Mocking된 값과 다른 값이 전달됨해결: 어차피 쓰지 않을 비밀번호이기에 전달된 값과는 상관없이 호출 여부만 검증함
Mockito의 anyString() 사용
anyString(): Mockito의 Argument Matcher 중 하나로, 메서드 호출 시 특정 타입의 인자가 어떤 값으로든 전달될 수 있음을 나타내기 위해 사용됨. String 타입의 값이 어떤 것이든 허용함수정된 테스트 코드
@Test
void createUserTest_KakaoSignupSuccess() {
// Given
String encodedPassword = "encodedRandomPassword";
CreateUserRequestDto request = CreateUserRequestDto.builder()
.email("test@test.com")
.password(null) // 비밀번호는 랜덤으로 생성
.nickname("kakaoUser")
.phone("010-5678-1234")
.birth(LocalDate.parse("1998-03-17"))
.imageProfile("kakaoProfile.png")
.lat(37.5665f)
.lng(126.9780f)
.address("상세주소")
.kakaoId(123456789L) // 카카오 ID 설정
.build();
// Mocking: 이메일과 닉네임 중복 없음, 비밀번호는 존재여부 확인
when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty());
when(userRepository.findByNickname(request.getNickname())).thenReturn(Optional.empty());
when(passwordEncoder.encode(anyString())).thenReturn(encodedPassword);
// When
ResponseDto response = userService.createUser(request);
// Then
assertEquals(1, response.getCode());
assertEquals("회원가입이 완료되었습니다", response.getMsg());
// Verify: 저장된 사용자 정보 확인
verify(userRepository).save(argThat(user ->
user.getEmail().equals("test@test.com") &&
user.getNickname().equals("kakaoUser") &&
user.getKakaoId().equals(123456789L) &&
user.getSocialProvider().equals("KAKAO") &&
user.getPassword().equals(encodedPassword)
));
}
일반 회원가입 성공
@Test
void createUserTest_Success() {
// Given
CreateUserRequestDto request = CreateUserRequestDto.builder()
.email("test@test.com")
.password("password") // 일반 유저의 비밀번호
.nickname("nickname")
.phone("010-1234-5678")
.birth(LocalDate.parse("1990-01-01"))
.imageProfile("profile.png")
.lat(37.5665f)
.lng(126.9780f)
.address("상세주소")
.kakaoId(null) // 카카오 ID 없음
.build();
// Mocking: 이메일과 닉네임 중복 없음
when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty());
when(userRepository.findByNickname(request.getNickname())).thenReturn(Optional.empty());
// Mocking: 비밀번호 암호화
String password = "password";
when(passwordEncoder.encode("password")).thenReturn(password);
// When
ResponseDto response = userService.createUser(request);
// Then
assertEquals(1, response.getCode());
assertEquals("회원가입이 완료되었습니다", response.getMsg());
// Verify: 저장된 사용자 정보 확인
verify(passwordEncoder).encode("password"); // 일반 유저 비밀번호 인코딩 검증
verify(userRepository).save(argThat(user ->
user.getEmail().equals("test@test.com") &&
user.getNickname().equals("nickname") &&
user.getKakaoId() == null &&
user.getSocialProvider().equals("NONE") &&
user.getPassword().equals(password) // 암호화된 비밀번호 저장 확인
));
}
조건문이 많으면 테스트 코드 짜기가 번거롭다는걸 느꼈다..
다 작성하고 보니 CreateUserRequestDto를 생성하는 부분이 번복되어서 따로 분리하였다.
private CreateUserRequestDto createRequest(String email, String password, String nickname, Long kakaoId) {
return CreateUserRequestDto.builder()
.email(email)
.password(password)
.nickname(nickname)
.phone("010-1234-5678")
.birth(LocalDate.parse("1990-01-01"))
.imageProfile("profile.png")
.lat(37.5665f)
.lng(126.9780f)
.address("상세주소")
.kakaoId(kakaoId)
.build();
}
그리고 카카오 로그인 때문에 password 필드를 notnull 설정해두지 않아서 에지 케이스로 추가 작성해줬다.
비밀번호가 Null인 경우
@Test
void createUser_WhenPasswordIsNullForGeneralUser() {
// Given
CreateUserRequestDto request = createRequest("test@test.com", null, "nickname", null);
// Mocking: 이메일과 닉네임 중복 없음
when(userRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty());
when(userRepository.findByNickname(request.getNickname())).thenReturn(Optional.empty());
// When
ResponseDto response = userService.createUser(request);
// Then
assertEquals(-1, response.getCode());
assertEquals("비밀번호가 없습니다", response.getMsg());
}