Mock? Stub? Spy? 헷갈리는 테스트 가짜 객체 3형제

Jun_k·2026년 2월 1일

Spring

목록 보기
4/9
post-thumbnail

단위 테스트의 본질 (Why)

단위 테스트(Unit Test)의 목표는 단 하나입니다.

내가 만든 로직만 검증한다. (격리)

  • 내가 테스트하려는 '서비스 로직'이 'DB'나 '외부 API' 때문에
    실패하면 안 되기에 가짜 대역(Test Double)을 세우는 것입니다.

Test Double이란?

  • 진짜 객체 대신 세워두는 가짜 객체이고,
    Mockito는 이 가짜 객체를 만드는 도구입니다.

Mock (가짜 객체), Stub (상태 고정), Spy (관찰자/스파이)

① Mock (가짜 객체)

  • 아무 로직도 없는 가짜 객체
UserRepository userRepository = mock(UserRepository.class);

userRepository.save(user);
  • DB 저장 ❌
  • 내부 로직 ❌
  • 그냥 호출만 기록된다. ✅
  • "이 메서드가 실행되었는가?"라는 행위 검증에 집중합니다.

② Stub (상태 고정)

  • 특정 호출에 대해 미리 정해진 답을 내놓도록 만든 상태입니다.

  • "A라는 입력이 들어오면 B를 뱉어라"라고,
    이 호출에는 이 값을 반환해라”라는 상태 설정

  • Stub이 필요한 이유는 다음 로직으로 테스트를 진행하려면
    반드시 "결과값"이 필요할 경우 입니다.

  • Mockito에서는 Mock 객체에 when().thenReturn()
    사용하는 행위 자체가 Stubbing입니다.

// save()가 호출되면, 실제 로직은 실행하지 말고, savedUser를 반환해라.
when(userRepository.save(any()))
    .thenReturn(savedUser);
  • ❗해당 코드가 필요한 이유는
    userRepository.save(user) 코드를 그대로 실행하게 되면
    DB 연결 시도, 트랜잭션 열림, INSERT SQL 실행 등
    단위 테스트에서 절대 원하지 않는 일이기에
    정해진 값을 반환해주는 것입니다.

  • ✅ “Mock인 UserRepository에 Stubbing을 했다”


③ Spy (관찰자/스파이)

  • 실제 객체를 감싸는 래퍼(Wrapper)입니다.

  • 진짜 객체를 기반으로 만든 가짜

  • 기본적으로 진짜 객체의 로직을 수행합니다.
    하지만 특정 메서드만 가짜 답을 내놓게 하거나,
    어떤 메서드가 몇 번 불렸는지 몰래 감시할 수 있습니다.

  • 실제 로직을 써야 하지만 일부만 제어하고 싶을 때 사용합니다.
    (정말 필요한 경우에만 사용)

// 실제 객체를 감싼 래퍼
// 기본 동작 = 진짜 로직 실행
Calculator calculator = spy(new Calculator());

// Spy 전용 문법
// 실제 메서드를 아예 타지 않게 막는 방식
// applyCoupon을 실행하지 말고, 그냥 1000을 반환해라
doReturn(1000)
    .when(calculator)
    .applyCoupon(any());
  • 🚧 Spy가 위험한 이유

    • 진짜 로직이 실행됨

      • 내부에서 DB 조회, 내부 상태 변경등이 발생하기에 이것은
        단위 테스트가 아닌 통합 테스트가 되어 버린다.

    • 의존성이 있으면 테스트가 깨짐

      class Calculator {
         private DiscountPolicy policy;
      }
      • policy가 null이면 NPE 발생하고, 해당 테스트는 환경 때문에 실패한다.
      • 테스트 실패 원인을 추적하기 어려워집니다.

    • 상태가 섞이기 쉬움

      // 상태를 가진 클래스
      class Counter {
       int count = 0;
      
       void increase() {
           count++;
       }
      
       int getCount() {
           return count;
       }
      }
      Counter counter = spy(new Counter());
      • 만약 Spy로 감싸면 count는 0이고, 진짜 객체 1개 생성 되면서
        이후 호출은 실제 필드의 영향을 주게 됩니다.
        호출할수록 count가 증가하니 테스트는 독립적이여야 하는데
        테스트 A가 남긴 흔적테스트 B의 결과바꿔버리는 것입니다.

어떤 상황에서 무엇을 써야할까?

상황 A: 회원가입 서비스 테스트 (Mock & Stub)

회원가입 로직에서 UserRepository.save()를 호출해야 합니다.
하지만 진짜 DB에 저장할 필요는 없습니다.

  • 선택: UserRepositoryMock으로 만듭니다.

  • Stubbing: save()가 호출되면 가짜 User 객체를 리턴하라고 설정합니다.

  • 검증: 최종적으로 verify(userRepository).save(any())를 통해
    실제로 저장 메서드가 호출됐는지 확인합니다.

  • verify는 테스트 로직이 끝난 후에 호출됩니다.
    아까 실행한 로직 중에 userRepositorysave가 실제로 호출됐는지 확인하는 것


상황 B: 외부 결제 API 연동 (Mock)

결제 버튼을 눌렀을 때 외부 카드사 API가 호출되는지 확인해야 합니다.

  • 선택: 결제 클라이언트를 Mock으로 만듭니다.

  • 이유: 진짜 카드 결제가 일어나면 안 되기 때문입니다.
    오직 "카드사 서버로 요청을 보냈는가?"만 확인합니다.


상황 C: 복잡한 계산기가 포함된 서비스 (Spy)

금액을 계산하는 Calculator 클래스가 있고, 이를 사용하는 OrderService를 테스트합니다.
계산 로직은 너무 정확해서 그대로 쓰고 싶은데,
특정 '이벤트 쿠폰' 적용 로직만 가짜로 처리하고 싶을 때가 있습니다.

  • 상황 설명

    • Calculator라는 클래스 안에 메서드가 100개 있다고 가정
      -> 99개는 너무 완벽하게 잘 만들어서 그대로 쓰고 싶음 (예: 덧셈, 곱셈, 세금 계산 등)

    • 그런데 딱 1개, applyCoupon(쿠폰 적용) 메서드만 테스트하기가 까다롭습니다.
      (예: 오늘 날짜에 따라 결과가 달라지거나, 외부 시스템을 확인해야 하는 경우)

    • 이때 Mock을 쓰면 빈 껍데기라 99개의 완벽한 로직도 다 새로
      가짜 답(Stub)을 만들어줘야 합니다. (번거로운 작업 필요)

    • 이때 Spy를 쓰면?
      나머지 99개는 그대로 사용하고, 쿠폰 적용만 내가 원하는 대로
      설정이 가능합니다.

    • 전체 로직 (실제): 주문금액 = (단가 * 수량) + 배송비

      • 이건 Calculator의 실제 로직을 그대로 믿고 씁니다.
    • 부분 로직 (가짜): - 할인금액(쿠폰)

      • 이 부분만 내가 원하는 값(예: 5,000원)으로 고정합니다.
  • 실행 과정

    • Spy 생성: Calculator의 실제 인스턴스를 spy()로 감쌉니다.

    • 조종(Stubbing): applyCoupon()이 불리면 무조건 5000을 반환하라고 명령합니다.

    • 테스트 실행: OrderService에서 계산기를 돌려봅니다.

    • 결과: 단가, 수량, 배송비 계산은 실제 코드가 돌아가서 정확한 값이 나오고,
      마지막에 쿠폰 할인만 내가 설정한 5,000원이 딱 적용됩니다.


간단 요약

  • 의존하는 객체가 너무 무겁거나(DB), 외부와 연결되어 있다면? → Mock을 만들어라

  • Mock특정 값을 반환해서 다음 로직을 진행시켜야 한다면? → Stubbing(when...thenReturn)

  • 실제 로직을 돌려야 하는데, 딱 한 두 메서드만 가짜로 바꾸거나
    호출 횟수를 재고 싶다면? → Spy를 사용하라

profile
개발을 즐겨보자.

0개의 댓글