Spring

[Spring TDD] Mockito의 Mock, Stub, Spy

leejunkim 2025. 8. 18. 09:46

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)