1. JUnit5 vs Mockito
JUnit은 자바에서 가장 널리 사용되는 단위 테스트 프레임워크다.
@Test, @BeforeEach, @AfterEach 등을 활용하여 테스트를 수행하는데 @Test가 단위 테스트 메서드를 정의하고 @BeforeEach는 각 테스트 전에 실행할 코드, @AfterEach는 각 테스트 후에 실행할 코드, @Nested는 내부 클래스를 활용한 그룹 테스트이다.
Mockito는 단위 테스트에서 Mock 객체를 만들어주는 라이브러리다.
실제 DB나 외부 API 없이 테스트할 수 있도록 지원한다.
mocking을 통해 의존성을 분리하여 테스트를 수행할 수 있는데 @Mock은 Mock 객체를 생성하고 @InjectMocks는 Mock 객체를 주입받는 클래스를 설정하고 when().thenReturn()은 특정 메서드의 반환값을 지정하고, verify()는 특정 메서드가 호출되었는지 검증한다.
즉, JUnit은 테스트 프레임워크이고 Mockito는 테스트에서 필요한 Mock 객체를 만들어주는 라이브러리다.
두 가지 방식으로 간단하게 UserService에 대 테스트 코드를 작성해보려고 한다.
2. JUnit5로 UserService 테스트 코드 작성하기
JUnit5로 통합 테스트를 작성하려고 한다. 이전에 작성한 Cache 관련 테스트가 JUnit 통합 테스트이긴 하지만 기능에 대한 테스트를 추가하였다.
@Test
void testGetUser_Success() {
UserDto userDto = userService.getUser(savedUser.getUserId());
assertThat(userDto)
.usingRecursiveComparison()
.ignoringFields("isRegistered", "createdAt", "updatedAt")
.isEqualTo(savedUser);
}
통합 코드는 간결하게 동일하게 CRUD 테스트로 작성하였으나 자꾸만 에러가 발생하였고 이상한 데이터로 인해 값이 다르게 호출되었다.
이는 다른 테스트로부터의 찌꺼기 값들이 잘 비워지지 않아서 발생한 문제였다.
두 개의 코드를 추가하였는데 먼저 cache를 삭제하지 않으면 다른 통합 테스트 실행 시 남아있는 데이터로 인해 에러가 발생한다.
그래서 before, after에 각각 redis DB의 데이터를 지우도록 설정하였다.
@BeforeEach
void setUp() {
userRepository.deleteAll();
redisTemplate.getConnectionFactory().getConnection().flushAll();
savedUser = userRepository.save(
User.builder()
.name("Haru")
.email("haru.every@example.com")
.provider("Google")
.providerUid("1234")
.accessToken("access_token")
.refreshToken("refresh_token")
.isRegistered(true)
.build()
);
userRepository.flush();
}
@AfterEach
void clearCacheAfterTest() {
redisTemplate.getConnectionFactory().getConnection().flushAll();
}
3. Mockito로 테스트 코드 작성하기
DB도 안 가고 데이터도 mock으로 만드는데 그럼 뭘 테스트하는건가?
Mockito를 통해 서비스 계층의 비즈니스 로직을 검증할 수 있다.
닉네임 중복 체크 로직이나 메서드의 실행, 예외 처리, 호출 여부 등을 검증할 수 있다.
@ExtendWith(MockitoExtension.class)
class UserServiceMockitoTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User mockUser;
@BeforeEach
void setUp() {
mockUser = User.builder()
.userId(1L)
.name("haru")
.email("haru.every@example.com")
.provider("Google")
.providerUid("1234")
.isRegistered(true)
.accessToken("access_token")
.refreshToken("refresh_token")
.birth(new java.sql.Date(2000, 1, 1))
.nickname("harus")
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
}
}
UserService는 UserRepository를 사용해서 데이터를 조회하는데 실제 DB를 사용하지 않고 Mockito를 사용해 가짜 데이터로 테스트하는 것이 목적이다.
즉, UserService에 UserRepository Mock을 주입하는 것이다.
@ExtendWith(MockitoExtension.class)는 JUnit5와 Mockito 통합을 위해 사용하는데 이걸 추가하면 @Mock, @InjectMocks 등의 Mockito 어노테이션이 자동으로 동작하도록 지원한다.
@Test
void testGetUser_Success() {
Long userId = 1L;
when(userRepository.findByUserIdQueryDSL(userId)).thenReturn(Optional.of((mockUser)));
UserDto result = userService.getUser(userId);
assertNotNull(result);
assertEquals(mockUser.getUserId(), result.getUserId());
assertEquals(mockUser.getNickname(), result.getNickname());
verify(userRepository, times(1)).findByUserIdQueryDSL(userId);
}
먼저 when(...).thenReturn(...)으로 가짜 데이터를 생성한다.
실제 DB를 접근하는게 아니라 findByUserIdQueryDSL 호출 시 mockUser가 반환되도록 설정하였다.
둘 다 당연히 같은 값이 아닌가 하겠지만 이는 기능에 대한 테스트, 즉 getUser가 정확히 동작하는지, mapToDto 필드 매핑, findByUserIdQueryDSL 호출 횟수를 확인하기 위한 테스트이다.
실제로 테스트 코드를 작성하면서 놓쳤던 코드 수정도 할 수 있었다.
@Test
void testGetUser_UserNotFound() {
Long userId = 1L;
when(userRepository.findByUserIdQueryDSL(userId)).thenReturn(Optional.empty());
CustomException exception = assertThrows(CustomException.class, () -> userService.getUser(userId));
assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode());
}
해당 에러가 RuntimeException으로 throw하게 되어 있었는데 수정하였다.
'dev > 프로젝트' 카테고리의 다른 글
[프로젝트] 프론트엔드(React & Redux) (4) | 2025.02.07 |
---|---|
[프로젝트] JWT + Redis 전환 (2) | 2025.02.06 |
[프로젝트] QueryDSL 적용 (2) | 2025.01.29 |
[프로젝트] README 적용 (0) | 2025.01.28 |
[프로젝트] Spring Security (2) | 2025.01.28 |