테스트 더블

david1-p·2025년 12월 2일

CS 지식 창고

목록 보기
26/26
post-thumbnail

백엔드 개발을 하며 테스트 코드를 작성하다 보면, 실제 데이터베이스나 외부 API와 같은 '의존성'을 그대로 사용하기 어려운 경우가 많습니다. 이때 등장하는 개념이 바로 테스트 더블(Test Double)입니다.

1. 테스트 더블이란?

영화 촬영에서 위험한 장면을 대신 연기하는 스턴트 더블(Stunt Double)이 있듯이, 테스트 더블은 실제 의존성 객체를 대신하여 테스트에 활용되는 모든 객체를 통칭하는 용어입니다.

왜 사용할까요?

실제 의존성을 테스트에 그대로 포함시키면 다음과 같은 문제가 발생할 수 있습니다.

  • 비결정적 동작: 외부 API 서버가 다운되었거나 네트워크 문제로 테스트가 실패할 수 있습니다.
  • 속도 저하: DB I/O나 네트워크 통신은 테스트 속도를 느리게 만듭니다.
  • 부수 효과(Side Effect): 테스트 도중 실제 이메일이 발송되거나, 운영 DB 데이터가 변경될 위험이 있습니다.
  • 복잡한 설정: 의존성 객체를 생성하기 위해 수많은 설정이 필요할 수 있습니다.

테스트 더블은 이러한 외부 요인으로부터 테스트를 격리시켜 빠르고, 안정적이며, 독립적인 테스트 환경을 만들어 줍니다.


2. 테스트 더블의 5가지 종류

테스트 더블은 그 역할과 복잡도에 따라 크게 5가지(Dummy, Stub, Fake, Spy, Mock)로 분류됩니다. (제라드 메스자로스의 분류 기준)

1) 더미 (Dummy)

  • 개념: 가장 기본적인 형태입니다. 객체가 필요하지만 실제로 기능은 전혀 필요 없는 경우에 사용합니다.
  • 용도: 주로 인자 리스트를 채우기 위해 사용되며, 메서드가 호출되어도 아무런 동작을 하지 않거나 예외를 던집니다.
  • 예시: 생성자의 매개변수로 필요하지만, 테스트 로직에는 전혀 영향을 주지 않는 객체.
// 단순히 컴파일 에러를 피하기 위해 넘겨주는 껍데기
User dummyUser = new User(); 
boardService.createBoard(dummyUser, "제목"); 

2) 스텁 (Stub)

  • 개념: 더미보다 한 단계 발전한 형태로, 미리 준비된 답변을 제공하는 객체입니다.
  • 용도: 테스트 호출에 대해 미리 정의해 둔 결과를 반환합니다. 로직은 없으며, 단순히 원하는 데이터 상태를 만들어줍니다.
  • 예시: "ID가 1인 사용자를 조회하면, 무조건 Alice 객체를 반환해라"라고 설정.
// UserRepository의 스텁 구현
public class StubUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // DB 조회 없이 무조건 미리 준비된 객체 반환
        return new User(1L, "Alice");
    }
}

3) 페이크 (Fake)

  • 개념: 실제 동작하는 구현을 가지고 있지만, 프로덕션(운영) 환경에는 적합하지 않은 객체입니다.
  • 용도: 로직이 실제로 돌아가기 때문에 실제 객체와 가장 유사하게 동작합니다. 하지만 성능이나 메모리 문제로 실제로는 쓰지 않습니다.
  • 예시: 실제 DB 대신 HashMap이나 ArrayList를 사용하여 메모리 상에서만 데이터를 저장하고 조회하는 가짜 리포지토리(In-Memory Database).
// 실제 DB 대신 Map을 사용하는 Fake 객체
public class FakeUserRepository implements UserRepository {
    private Map<Long, User> data = new HashMap<>();

    @Override
    public void save(User user) {
        data.put(user.getId(), user);
    }
    
    @Override
    public User findById(Long id) {
        return data.get(id);
    }
}

4) 스파이 (Spy)

  • 개념: 자신이 호출된 내역을 몰래 기록하는 객체입니다. 스텁의 역할을 하면서, 추가적으로 호출 정보를 기록합니다.
  • 용도: 메서드가 몇 번 호출되었는지, 어떤 인자가 넘어왔는지 등을 검증할 때 사용합니다. 실제 객체를 감싸서(Wrapper) 사용할 수도 있습니다.
  • 예시: 이메일 발송 서비스가 실제로 호출되었는지, 호출되었다면 수신자가 누구였는지 확인.
// 호출 여부를 기록하는 Spy
public class SpyEmailService implements EmailService {
    public int sendCount = 0; // 호출 횟수 기록
    public String lastMessage = null;

    @Override
    public void send(String message) {
        this.sendCount++;
        this.lastMessage = message;
    }
}

5) 목 (Mock)

  • 개념: 행위(Behavior)를 검증하기 위해 사용되는 객체입니다.
  • 용도: "이 메서드가 호출되어야 한다"는 기대(Expectation)를 미리 정의해두고, 테스트가 끝난 후 그 기대대로 동작했는지 확인합니다. 기대와 다르게 동작하면 예외를 발생시킵니다.
  • 특징: 목 프레임워크(Mockito 등)를 사용하면 스텁과 스파이의 기능을 모두 포함하는 강력한 기능을 제공합니다.
// Mockito를 활용한 Mock 예시
// 1. Mock 생성
EmailService mockEmailService = mock(EmailService.class);

// 2. 행위 수행
orderService.order();

// 3. 행위 검증 (verify): send()가 1번 호출되었는지 확인
verify(mockEmailService, times(1)).send(anyString());

3. 핵심 구분: 상태 검증 vs 행위 검증

테스트 더블을 이해할 때 가장 중요한 기준은 "무엇을 검증하는가?"입니다.

  1. 상태 검증 (State Verification):

    • 메서드 실행 후, 객체의 상태(데이터 값)가 어떻게 변했는지 확인합니다.
    • 주로 Stub, Fake, Spy를 사용하여 확인합니다.
    • 예: save() 호출 후 findById()로 조회했을 때 데이터가 잘 들어있는가?
  2. 행위 검증 (Behavior Verification):

    • 메서드가 실행될 때, 의존하고 있는 다른 객체와 올바르게 상호작용(호출) 했는지 확인합니다.
    • 주로 Mock을 사용하여 확인합니다.
    • 예: order() 호출 시 emailService.send()가 정확히 1번 호출되었는가?

Tip: 보통 실무에서는 Stub으로 상태를 세팅하고, Mock으로 행위를 검증하는 방식이 혼합되어 사용됩니다. 최근의 Mockito 같은 프레임워크는 Mock 객체 하나로 스텁(Stubbing)과 검증(Verifying)을 모두 처리할 수 있어 경계가 모호해지기도 했지만, 개념적으로는 분리해서 이해하는 것이 좋습니다.


4. 요약

테스트 더블은 외부 세계의 불확실성을 제거하고 오직 나의 로직(System Under Test)에만 집중할 수 있게 해주는 강력한 도구입니다.

  • 단순한 데이터 주입이 필요하면 Dummy
  • 테스트를 위한 특정 결과값이 필요하면 Stub
  • 가벼운 로직 구현체가 필요하면 Fake
  • 호출 내역을 확인하고 싶다면 Spy
  • 올바르게 호출되었는지 행위를 검증하고 싶다면 Mock

각각의 특징을 잘 이해하고 상황에 맞는 적절한 테스트 더블을 선택한다면, 훨씬 더 견고하고 유지보수하기 쉬운 테스트 코드를 작성할 수 있습니다.


profile
DONE IS BETTER THAN PERFECT.

0개의 댓글