본문 바로가기
dev/프로젝트

[프로젝트] 테스트 코드 적용

by dev-everyday 2025. 1. 30.
반응형

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