본문 바로가기
codeit sprint backend/weekly paper

[10-2] Mockito

by boolynn 2026. 4. 7.

✔️ Mockito

테스트를 작성할 때 외부에 의존하게 되면 테스트가 느려지고, 환경에 따른 결과가 달라질 수 있다. 이러한 문제를 해결하기 위해 Spring에서는 Moking 프레임워크로 Mockito를 주로 사용한다. 즉, 테스트 하려는 객체가 의존하는 객체를 사용하는 것이 아니라 가짜 객체(Mock)을 만듦으로써 외부 환경에 영향을 받지 않도록 하여 검증하고 싶은 로직만 검증할 수 있도록 하는 방식이다. Mockito는 Mock, Stub, Spy를 제공하는데 모두 진짜 객체 대신 사용되는 가짜 객체이지만, 그 목적과 사용 방식에서 차이를 갖는다.

 

✔️ Mock vs Stub vs Spy

Mock - 행위 검증

  • 모든 메소드가 기본적으로 아무것도 하지 않고 null이나 기본값을 반환하는 완전한 가짜 객체
  • 행위 검증(특정 메소드가 호출되는지, 몇 번 호출되는지 등)이 목적
  • @Mock를 통해 구현
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private EmailNotificationService emailService;
    
    @Mock
    private OrderRepository orderRepository;
    
    @InjectMocks
    private OrderService orderService;

    @Test
    void 주문_생성_시_이메일_알림이_발송된다() {
        // given
        Order mockOrder = createTestOrder();
        given(orderRepository.save(any())).willReturn(mockOrder);

        // when
        orderService.createOrder(request);

        // then: "이 메소드가 정확히 한 번 호출됐는가?" 가 핵심
        verify(emailService, times(1))
            .sendOrderConfirmation(eq("customer@test.com"), any(Order.class));
        
        // 호출되지 않았어야 하는 것도 검증 가능
        verify(emailService, never()).sendCancellationEmail(any());
    }
}

 

Stub - 상태 검증

  • 특정 메소드가 호출됐을 때 미리 정해둔 값을 반환하도록 설정
  • 상태 검증(이 상태에서 시스템이 올바르게 반응하는가)이 목적
  • 주로 Mock과 함께 사용됨
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private ProductRepository productRepository;
    
    @Mock  
    private CustomerRepository customerRepository;

    @InjectMocks
    private OrderService orderService;

    @Test
    void 재고_부족_시_예외가_발생한다() {
        // Stub: 메소드 호출 시 원하는 값을 반환하도록 임의로 설정
        Product soldOutProduct = Product.builder()
                .id("PROD-001")
                .stockQuantity(0)  // 재고 없음
                .build();
        
        // given → willReturn 패턴이 Stub의 핵심
        given(productRepository.findById("PROD-001"))
                .willReturn(Optional.of(soldOutProduct));
        
        given(customerRepository.findByEmail(any()))
                .willReturn(Optional.of(activeCustomer()));

        // when & then: 반환된 "상태"에 따른 결과를 검증
        assertThatThrownBy(() -> orderService.createOrder(request))
                .isInstanceOf(InsufficientStockException.class)
                .hasMessageContaining("재고 부족");
    }

    @Test
    void 여러_시나리오_Stub_설정() {
        // 연속 호출 시 다른 값 반환
        given(productRepository.findById(any()))
                .willReturn(Optional.of(availableProduct()))  // 첫 번째 호출
                .willReturn(Optional.empty());                // 두 번째 호출

        // 예외 던지기
        given(productRepository.findById("INVALID"))
                .willThrow(new DatabaseException("연결 오류"));
    }
}

 

Spy - 실제 객체의 부분 모킹

  • 실제 객체를 기반으로 동작하되 특정 메소드만 원하는 동작으로 교체
  • 실제 구현 로직은 그대로 활용하면서 외부 시스템 호출과 같은 특정 부분만 차단하고 싶은 경우 사용
  • 남용하게 되면 테스트의 외부 의존도가 높아지므로, 적절히 사용하는 것이 중요
  • @Spy를 통해 구현
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Spy
    private OrderValidator orderValidator = new OrderValidator();  // 실제 객체!
    
    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

    @Test  
    void Spy_실제_동작_유지하면서_일부만_모킹() {
        // Spy: 실제 객체를 사용하되, 특정 메소드만 가로채기
        // 나머지 검증은 실제 로직 실행, 외부 호출만 모킹
        doReturn(true).when(orderValidator).isWithinDailyLimit(any());
        
        // 실제 validateFormat() 등은 진짜 실행됨
        Order order = Order.create(product, customer, 5);
        
        // 특정 메소드 호출 여부도 검증 가능 (Mock처럼)
        verify(orderValidator).isWithinDailyLimit(any());
    }

    @Test
    void Spy_주의사항_실제_메서드_호출_순서() {
        OrderValidator spy = spy(new OrderValidator());
        
        // 잘못된 방식: given().willReturn() 사용 시 설정 전에 실제 메소드가 먼저 실행됨
        given(spy.expensiveValidation(any())).willReturn(true);
        
        // 올바른 방식: doReturn을 사용해 실제 호출 방지
        doReturn(true).when(spy).expensiveValidation(any());
    }
}

 

*예시 코드는 ai를 활용함