WeeklyPaper: 테스트에서 사용되는 Mockito의 Mock, Stub, Spy 개념을 각각 설명하고, 어떤 상황에서 어떤 방식을 선택해야 하는지 구체적인 예시와 함께 설명하세요.

Mockito
테스트 코드를 짜기 시작할때 내 코드가 다른 클래스나 외부 시스템에 의존하고 있으면 의존성의 문제가 생긴다. 순수하게 내 로직만 테스트하기 어려워진다. 이럴때 필요한 것이 바로 테스트 더블 (Test double)이고, Mockito는 Java 진영에서 가장 널리 사용되는 테스트 더블 생성 라이브러리다.
- Mockito는 테스트 더블인 Mock, Stub, Spy를 대표적으로 제공한다.
- 스프링에서는 @MockitoBean 으로 Mockito와 자연스럽게 통합되어 있다. (@MockBean은 현재 Deprecated되어있다)
Mock / Stub / Spy 차이
| 항목 | Mock | Stub | Spy |
| 목적 | 행위 검증 (Behavior Verification) | 상태 검증 (State Verification) | 부분적인 행위 변경 및 검증 |
| 핵심 | 객체의 메서드가 올바르게 호출되었는지 (횟수, 순서, 인자 등)를 검증 | 테스트에 필요한 정해진 값을 반환하는 것 (상태를 미리 준비해두는 개념) | 실제 객체를 기반으로 동작하며, 특정 메서드만 가짜로 대체하고 나머지는 실제 로직을 수행하게 함 |
| 구현 | - mock() 메서드를 통해 인터페이스나 클래스로부터 가짜 객체를 생성함 - 내부 로직이 없는 껍데기입니다. |
- 테스트에 필요한 동작을 하드코딩하여 직접 구현함 - 또한 Mockito의 when(...).thenReturn(...)을 사용해 단순한 반환 값을 설정함 |
- spy() 메서드로 실제 객체를 감싸서 생성 - 실제 메서드를 호출하면서도 필요에 따라 일부를 가짜로 바꿀 수 있음 |
| 사용 시점 | - 외부 시스템과의 상호작용이 올바르게 일어나는지 검증해야 할 때 - ex: 이메일 발송, DB 저장 요청 |
- 특정 조건에 따라 정해진 데이터를 반환해야 테스트 로직을 완성할 수 있을 때 - ex: Repository에서 특정 데이터 조회 |
- 오래되었거나 테스트하기 어려운 레거시 코드를 다룰 때 - 혹은 대부분의 실제 동작은 유지한 채 특정 부분만 격리하고 싶을 때 유용함 |
| 테스트 위치 | - 단일 테스트(Unit Test) - 슬라이스 테스트 (Slice Test) |
- 단일 테스트(Unit Test) | (제한적) - 단일 테스트(Unit Test) - 통합 테스트 (IntegrationTest) |
Stub
가장 단순한 형태의 테스트 더블이다.호출하면 미리 준비된 데이터를 반환하는 역할에만 충실하다.
그냥 정해진 대사만 말하는 배우라고 생각하면 쉽다.
@Test
void whenUserIdExists_thenUserNameShouldBeCapitalized() {
// ========================= given =========================
// 1. Stub 객체 생성 및 행동 정의
UserRepository stubUserRepository = mock(UserRepository.class);
User fakeUser = new User("luckykim", "김러키");
// "luckykim" ID로 findById를 호출하면, 항상 fakeUser를 반환하도록 설정
when(stubUserRepository.findById("luckykim")).thenReturn(Optional.of(fakeUser));
// 2. 테스트 대상 객체에 Stub 주입
UserService userService = new UserService(stubUserRepository);
// ========================= when =========================
String capitalizedUserName = userService.getCapitalizedUserName("luckykim");
// ========================= then =========================
// "김러키"가 "김러키"로 올바르게 변환되었는지 확인
assertThat(capitalizedUserName).isEqualTo("김러키");
}
- 상황: 내 서비스 로직이 userRepository.findById()를 호출했을 때, 특정 User 객체가 반환되어야만 다음 로직을 테스트할 수 있다. 반환되는 값 자체가 중요한 케이스다.
- 이처럼 단순히 정해진 값을 반환하기만 하면 된다면 stub을 사용하면 된다. Mockito에서는 mock 객체에 when().thenReturn()을 사용해 Stub을 만든다.)
Mock
Mock은 Stub의 역할도 포함하면서, 더 나아가 객체의 행위까지 검증한다. 특정 메서드가 예상대로 호출되었는지, 몇번 호출되었는지, 어떤 인자로 호출되었는지 등 꼼꼼하게 감시할 수 있다.
@Test
void whenUserRegisters_thenWelcomeEmailShouldBeSent() {
// ========================= given =========================
// 1. Mock 객체 생성
EmailService mockEmailService = mock(EmailService.class);
UserRepository stubUserRepository = mock(UserRepository.class); // 여기선 단순 Stub으로 사용
// 2. 테스트 대상 객체에 Mock 주입
UserService userService = new UserService(stubUserRepository, mockEmailService);
// ========================= when =========================
userService.register("user@example.com", "password123");
// ========================= then =========================
// mockEmailService의 sendEmail 메서드가 "user@example.com"과 "가입을 환영합니다"라는 인자로
// 정확히 1번 호출되었는지 검증
verify(mockEmailService, times(1)).sendEmail("user@example.com", "가입을 환영합니다");
}
- 상황: 회원가입 로직이 끝날 때, emailService.send()나 notificationManager.notify() 같은 외부 시스템 호출 메서드를 정확히 한 번, 올바른 내용으로 호출해야 한다. 반환값은 void라 중요하지 않다.
- "그래서... 호출은 했어? 몇 번? 뭐라고 말했는데?" 가 궁금하면 mock를 사용하고 verify로 검증하면 된다.
Spy
실제 객체를 감싸서 만든다. 기본적으로 모든 메서드가 실제 객체의 로직을 그대로 수행하지만, 필요에 따라 특정 메서드의 행동만 가짜로 바꿀 수 있다.
@Test
void whenOrderIsPlaced_thenPaymentShouldBeProcessedAndOrderIdGenerated() {
// ========================= given =========================
// 1. 실제 객체 기반으로 Spy 객체 생성
OrderService realService = new OrderService();
OrderService spyOrderService = spy(realService);
// 2. 특정 메서드만 Stubbing
// **주의**: Spy는 실제 메서드를 호출하므로 when(spy.method())... 대신 doReturn(...).when(spy).method() 구문을 사용해야 안전합니다.
doReturn(true).when(spyOrderService).processPayment(any(Order.class));
Order newOrder = new Order("item-123", 10000);
// ========================= when =========================
boolean isOrderSuccessful = spyOrderService.placeOrder(newOrder);
// ========================= then =========================
// 1. 최종 결과 검증
assertThat(isOrderSuccessful).isTrue();
// 2. 실제 메서드가 호출되었는지 상태 검증
assertThat(newOrder.getOrderId()).isNotNull();
// 3. Stubbing된 메서드가 호출되었는지 행위 검증
verify(spyOrderService, times(1)).processPayment(newOrder);
}
- 상황: 테스트하려는 클래스(OrderService)는 내부적으로 10개의 메서드를 호출하며 복잡한 로직을 수행한다. 이 로직들은 모두 테스트하고 싶지만, 그중 딱 하나, processPayment() 메서드가 외부 결제 API를 호출해서 테스트하기가 너무 힘들다.
- 실제 객체의 99%는 그대로 쓰고, 딱 1%만 내 마음대로 바꾸고 싶다면, spy를 사용하면 된다.
| 내가 원하는 것 🤔 | 선택은? ✅ | 핵심 이유 |
| 테스트에 필요한 데이터나 상태를 미리 설정하고 싶다. | Stub | State Verification (상태 검증) |
| 외부 의존 객체의 메서드가 올바르게 호출되었는지 검증하고 싶다. | Mock | Behavior Verification (행위 검증) |
| 실제 객체의 로직을 대부분 그대로 쓰면서, 특정 메서드만 가짜로 바꾸고 싶다. | Spy | Partial Mocking (부분 Mocking) |
'Spring' 카테고리의 다른 글
| [Spring] Spring Cache에서 @Cacheable, @CachePut, @CacheEvict의 차이점과 적절한 사용법 (0) | 2025.10.20 |
|---|---|
| [JPA] 트랜잭션의 ACID 속성 중 격리성(Isolation)이 보장되지 않을 때 (0) | 2025.07.23 |
| [JPA] N+1 문제 (4) | 2025.07.21 |
| [SpringBoot] @Controller와 @RestController의 차이점과 요청 처리 흐름 (0) | 2025.07.03 |
| [Spring] Spring AOP 개념과 실제 활용 사례 (0) | 2025.07.01 |