지난 8편에서는 Controller를 테스트하면서 MockMvc를 사용해 보았습니다.
이제 우리는 Repository, Service, Controller 모든 계층을 테스트할 수 있게 되었습니다.
하지만 개발을 하다 보면 이런 고민에 빠집니다.
"테스트를 짤 때마다 DB를 켜야 하나요?"
"메일을 보내는 기능은 실제로 메일을 보내면 안 되는데 어떻게 테스트하죠?"
이런 난관을 해결해 주는 것이 바로
Mockito
입니다.
이미 Service 테스트에서 살짝 맛보았지만, 오늘은 이 도구를 제대로 활용하여
객체 간의 관계(의존성)를 깔끔하게 분리하는 방법
을 깊이 있게 알아보겠습니다.
Mock(목)은 영화 촬영장의
스턴트맨
과 같습니다.
위험한 액션 씬(DB 연결, 결제 요청 등)을 주연 배우(실제 객체) 대신 처리해 줍니다.
테스트 세계에서 Mock은 다음과 같은 역할을 합니다.
외부 의존성 제거
DB, 네트워크, 파일 시스템 없이도 로직을 검증할 수 있게 해줍니다.
테스트 속도 향상
무거운 작업을 하지 않으므로 테스트가 순식간에 끝납니다.
예측 불가능한 상황 제어
"결제 서버가 다운되었을 때" 같은 어려운 상황을 강제로 만들어 테스트할 수 있습니다.
테스트를 검증하는 방법에는 크게 두 가지가 있습니다.
이 둘의 차이를 아는 것이 TDD 레벨업의 핵심입니다.
✅
1. 상태 검증 (State Verification) 🔗
우리가 지금까지 주로 해왔던 방식입니다.
메서드를 실행한
후
에, 결과값이나 객체의 상태가 기대한 대로 바뀌었는지 확인합니다.
알고리즘 문제에서 결과값이 정답과 같은지 확인하는 것과 같습니다.
src/test/java/com/example/tdd_study/product/StateTest.java(얘시)@Test
void 더하기_상태_검증() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
// 결과 상태(값)가 5인지 확인
assertThat(result).isEqualTo(5);
}
✅
2. 행위 검증 (Behavior Verification) 🔗
Mockito가 빛을 발하는 순간입니다.
결과값보다는 "어떤 메서드가 호출되었는가?", "몇 번 호출되었는가?"를 확인합니다.
리턴 값이 없는(void) 메서드나, 다른 객체와의 협력을 테스트할 때 사용합니다.
src/test/java/com/example/tdd_study/product/BehaviorTest.java(예시)@Test
void 메일_전송_행위_검증() {
// given
EmailService emailService = mock(EmailService.class);
UserService userService = new UserService(emailService);
// when
userService.register("user@example.com");
// then: sendWelcomeEmail 메서드가 호출되었는지 확인(verify)
verify(emailService).sendWelcomeEmail("user@example.com");
}
이것은 그래프 탐색 알고리즘(BFS/DFS)에서 "이 노드를 방문했는가?"를 체크하는 것과 비슷한 원리입니다.
우리의 ProductService에 새로운 요구사항이 생겼다고 가정해 봅시다.
상품을 등록하면 알림 센터(NotificationCenter)에 알림을 보내야 한다.
이 기능을 구현하기 위해서는 인터페이스 정의, 구현체 생성, 그리고 서비스 연결이 필요합니다.
먼저 알림 기능을 추상화한 인터페이스를 만듭니다.
src/main/java/com/example/tdd_study/notification/NotificationCenter.javapackage com.example.tdd_study.notification;
public interface NotificationCenter {
void send(String message);
}
이 단계가 빠지면 애플리케이션 실행 시
Bean을 찾을 수 없다는 에러
가 발생합니다.
실제 동작하는 구현체를 만들고 스프링 빈으로 등록해야 합니다.
src/main/java/com/example/tdd_study/notification/ConsoleNotificationCenter.javapackage com.example.tdd_study.notification;
import org.springframework.stereotype.Component;
@Component // 스프링이 관리하는 빈으로 등록
public class ConsoleNotificationCenter implements NotificationCenter {
@Override
public void send(String message) {
System.out.println("[알림 발송] " + message);
}
}
이제
ProductService가
NotificationCenter를 사용하도록 수정합니다.
생성자 주입 방식을 사용하므로 코드가 조금 바뀝니다.
src/main/java/com/example/tdd_study/product/ProductService.javapackage com.example.tdd_study.product;
import com.example.tdd_study.notification.NotificationCenter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductService {
private final ProductRepository productRepository;
private final NotificationCenter notificationCenter; // 추가된 의존성
// 생성자 주입
public ProductService(ProductRepository productRepository,
NotificationCenter notificationCenter) {
this.productRepository = productRepository;
this.notificationCenter = notificationCenter;
}
@Transactional
public void register(String name, int price) {
Product product = new Product(name, price);
productRepository.save(product);
// 알림 전송 로직 호출
notificationCenter.send("새로운 상품이 등록되었습니다: " + name);
}
}
이제
ProductServiceTest를 수정하여 알림이 제대로 발송되는지 테스트해 보겠습니다.
여기서는 실제
ConsoleNotificationCenter 대신
Mock 객체
를 사용하여
행위 검증
을 수행합니다.
src/test/java/com/example/tdd_study/product/ProductServiceTest.javapackage com.example.tdd_study.product;
import com.example.tdd_study.notification.NotificationCenter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@Mock // NotificationCenter를 가짜 객체(Mock)로 생성
private NotificationCenter notificationCenter;
@InjectMocks // 가짜 객체들을 Service에 주입
private ProductService productService;
@Test
void 상품_등록시_알림이_전송되어야_한다() {
// given
String name = "라떼";
// (중요) 6편에서 정한 도메인 규칙(1000원 단위)에 따라 4500이 아닌 4000이어야 합니다.
int price = 4000;
// when
productService.register(name, price);
// then
// 1. Repository의 save 메서드가 호출되었는지 검증
verify(productRepository).save(any(Product.class));
// 2. NotificationCenter의 send 메서드가 특정 메시지와 함께 호출되었는지 검증
verify(notificationCenter).send("새로운 상품이 등록되었습니다: 라떼");
}
}
@Mock
NotificationCenter의 껍데기만 만듭니다. 실제 구현체(ConsoleNotificationCenter)는 실행되지 않습니다.
verify(mock).method()
Mock 객체의 특정 메서드가 실행되었는지 감시합니다. 만약 register 메서드 안에서 send를 호출하지 않았다면 테스트는 실패합니다.
실행하여 초록색 체크 표시가 떴다면 성공입니다.
그런데 콘솔 창을 보면
아무런 로그도 찍히지 않습니다.
"어? 아까
ConsoleNotificationCenter에서
System.out.println을 썼는데 왜 안 나오죠?"
이것이 바로 Mocking의 핵심입니다.
우리는 실제 객체(
ConsoleNotificationCenter)가 아니라
가짜 객체(Mock)
를 주입했기 때문입니다.
가짜 객체는 아무런 기능이 없는 껍데기이므로 로그를 출력하지 않습니다.
대신
verify가 내부적으로 "메서드가 호출되었음"을 기록하고 검증에 성공했기 때문에 초록 불이 뜬 것입니다.
Mockito가 어떻게 에러를 잡는지 확인하기 위해, ProductService에서 알림 전송 코드를 주석 처리하고 테스트를 돌려봅시다.
// notificationCenter.send("..."); // 주석 처리
테스트를 실행하면 빨간 불과 함께 다음과 같은 에러 메시지가 뜹니다.

인위적으로 에러 발생 시 WantedButNotInvoked
Wanted but not invoked:
notificationCenter.send("새로운 상품이 등록되었습니다: 라떼");
-> at com.example.tdd_study.product.ProductServiceTest...
"원했는데(Wanted) 호출되지 않았다(not invoked)"라고 명확하게 알려줍니다.
이처럼 Mockito는 실제 코드를 실행하지 않고도 로직의 흐름(Flow)을 정확하게 감시할 수 있습니다.
Mock은 강력하지만 남용하면 독이 됩니다.
주의해야 할 점들을 정리해 드립니다.
과도한 Mocking 주의
모든 것을 Mock으로 만들면, 실제로는 동작하지 않는 껍데기 테스트
가 될 수 있습니다.
Value Object는 Mock하지 않기
String, List, 혹은 우리가 만든 Product 같은 데이터 객체는 Mock으로 만들지 말고 그냥 new로 생성해서 쓰세요.
Mock은 *행위(Service, Repository)*를 가진 객체에만 사용하는 것이 좋습니다.
구현 디테일에 의존
verify를 너무 꼼꼼하게 쓰면, 내부 로직을 조금만 바꿔도 테스트가 깨질 수 있습니다.
핵심적인 행위만 검증하세요.
오늘은 Mockito를 활용해 의존성을 분리하고 행위를 검증하는 방법을 알아보았습니다.
Mock의 역할
외부 시스템이나 복잡한 의존성을 대신하는 스턴트맨입니다.
구현체 등록
테스트에서는 Mock을 쓰지만, 실제 앱 구동을 위해 @Component가 붙은 구현체(ConsoleNotificationCenter)가 반드시 필요합니다.
도메인 규칙 준수
Mock 테스트라도 내부에서 실제 객체(Product)를 생성한다면, 그 객체의 검증 로직(1000원 단위 등)을 통과하는 데이터를 사용해야 합니다.
행위 검증
verify() 메서드를 사용하면 리턴 값이 없는 로직도 테스트할 수 있습니다.
다음 시간에는 테스트의 또 다른 중요한 부분인 예외 처리(Exception)와 유효성 검사(Validation)를 테스트하는 방법에 대해 알아보겠습니다.