본문으로 건너뛰기
Advertisement

18.2 Mockito로 Mock 테스트

Mockito는 자바의 대표적인 Mocking 프레임워크입니다. 실제 데이터베이스, 외부 API, 파일 시스템 등에 의존하지 않고 가짜 객체(Mock) 를 사용해 단위 테스트를 격리된 환경에서 실행할 수 있게 해줍니다.

1. 왜 Mock이 필요한가?

실제 테스트 상황:
UserService → UserRepository → Database

문제점:
- DB 연결이 없으면 테스트 불가
- 테스트마다 DB 상태가 달라 결과가 불안정
- DB 조회가 느려서 전체 테스트 시간이 늘어남

해결: Mock 객체로 DB 대체
UserService → MockUserRepository (가짜 DB)
- 항상 원하는 데이터 반환
- 빠르고 안정적
- 외부 의존성 없음

2. 설정

// build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.mockito:mockito-core:5.5.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.5.0'
}

3. 기본 사용법

테스트 대상 코드

// UserRepository.java (인터페이스)
public interface UserRepository {
Optional<User> findById(Long id);
Optional<User> findByEmail(String email);
User save(User user);
boolean existsByEmail(String email);
}

// User.java
public record User(Long id, String name, String email, int age) {
public User withId(Long id) {
return new User(id, name, email, age);
}
}

// UserService.java (테스트 대상)
public class UserService {
private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다: " + id));
}

public User registerUser(String name, String email, int age) {
if (userRepository.existsByEmail(email)) {
throw new IllegalStateException("이미 사용 중인 이메일입니다: " + email);
}
User newUser = new User(null, name, email, age);
return userRepository.save(newUser);
}
}

기본 Mockito 테스트

import org.junit.jupiter.api.*;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

@ExtendWith(MockitoExtension.class) // Mockito와 JUnit 5 통합
class UserServiceTest {

@Mock // Mock 객체 자동 생성
UserRepository userRepository;

@InjectMocks // Mock을 UserService에 자동 주입
UserService userService;

@Test
@DisplayName("ID로 사용자 조회 성공")
void getUserById_Success() {
// given: Mock 행동 설정 (stub)
User mockUser = new User(1L, "홍길동", "hong@example.com", 30);
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));

// when: 실제 실행
User result = userService.getUserById(1L);

// then: 검증
assertNotNull(result);
assertEquals("홍길동", result.name());
assertEquals("hong@example.com", result.email());

// Mock이 실제로 호출됐는지 검증
verify(userRepository).findById(1L);
verify(userRepository, times(1)).findById(1L); // 정확히 1번 호출
}

@Test
@DisplayName("존재하지 않는 ID로 조회 시 예외 발생")
void getUserById_NotFound() {
// given
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());

// when & then
assertThrows(IllegalArgumentException.class, () -> {
userService.getUserById(999L);
});
}

@Test
@DisplayName("회원가입 성공")
void registerUser_Success() {
// given
String email = "new@example.com";
User savedUser = new User(1L, "신규사용자", email, 25);

when(userRepository.existsByEmail(email)).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);

// when
User result = userService.registerUser("신규사용자", email, 25);

// then
assertNotNull(result);
assertEquals(1L, result.id());
assertEquals(email, result.email());

// 메서드 호출 순서 검증
InOrder inOrder = inOrder(userRepository);
inOrder.verify(userRepository).existsByEmail(email);
inOrder.verify(userRepository).save(any(User.class));
}

@Test
@DisplayName("중복 이메일로 가입 시 예외 발생")
void registerUser_DuplicateEmail() {
// given
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);

// when & then
assertThrows(IllegalStateException.class, () ->
userService.registerUser("홍길동", "existing@example.com", 30)
);

// save는 호출되면 안 됨
verify(userRepository, never()).save(any());
}
}

4. 주요 Mockito 기능

ArgumentMatchers (인자 매처)

// 특정 값
when(repo.findById(1L)).thenReturn(Optional.of(user));

// 어떤 값이든
when(repo.findById(anyLong())).thenReturn(Optional.of(user));
when(repo.findByEmail(anyString())).thenReturn(Optional.empty());
when(repo.save(any())).thenReturn(user);
when(repo.save(any(User.class))).thenReturn(user);

// 조건 있는 매처
when(repo.findById(argThat(id -> id > 0))).thenReturn(Optional.of(user));

예외 발생 설정

when(repo.findById(anyLong()))
.thenThrow(new RuntimeException("DB 연결 실패"));

// void 메서드에서 예외
doThrow(new RuntimeException("삭제 실패"))
.when(repo).deleteById(anyLong());

여러 호출에 다른 반응

when(repo.findById(anyLong()))
.thenReturn(Optional.of(user1)) // 첫 번째 호출
.thenReturn(Optional.of(user2)) // 두 번째 호출
.thenThrow(new RuntimeException()); // 세 번째 호출

verify - 호출 검증

// 호출 횟수 검증
verify(repo).findById(1L); // 정확히 1번
verify(repo, times(3)).findAll(); // 정확히 3번
verify(repo, atLeast(1)).findByEmail(any()); // 최소 1번
verify(repo, atMost(2)).save(any()); // 최대 2번
verify(repo, never()).deleteById(any()); // 한 번도 안 호출

// 특정 인자로 호출됐는지
verify(repo).save(argThat(u -> u.name().equals("홍길동")));

// 캡처: 어떤 인자로 호출됐는지 확인
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(repo).save(captor.capture());
User savedUser = captor.getValue();
assertEquals("홍길동", savedUser.name());

@Spy - 실제 객체 부분 Mock

// Spy: 실제 구현을 사용하되, 일부 메서드만 스텁
@Spy
ArrayList<String> spyList = new ArrayList<>();

@Test
void spyTest() {
spyList.add("A"); // 실제 add() 실행
spyList.add("B");
assertEquals(2, spyList.size()); // 실제 size() 실행

// 특정 메서드만 스텁
doReturn(100).when(spyList).size();
assertEquals(100, spyList.size()); // 스텁된 결과
}

고수 팁

Mock vs Stub vs Spy vs Fake:

종류설명Mockito
Mock예상 호출을 검증하는 가짜 객체@Mock / mock()
Stub정해진 값을 반환하는 가짜 객체when().thenReturn()
Spy실제 객체를 감싸서 일부만 스텁@Spy / spy()
Fake실제로 동작하는 간단한 구현체직접 구현

테스트 피라미드:

        /\
/E2E\ ← 적게 (비쌈, 느림)
/------\
/통합 테스트\ ← 중간
/----------\
/단위 테스트 \ ← 많이 (빠름, 저렴)
/--------------\

단위 테스트가 기반이며 Mockito로 격리합니다. 통합/E2E 테스트는 실제 DB 등을 사용합니다.

Spring Boot에서는 @SpringBootTest, @WebMvcTest, @DataJpaTest 어노테이션이 통합 테스트를 지원하며, @MockBean으로 Spring 컨텍스트 내 Mock 주입이 가능합니다.

Advertisement