Programming/Test

[Test] 테스트 더블(Test Double)

soeun2537 2025. 5. 3. 00:08
우아한테크코스 레벨 2에서 학습한 내용을 정리한 글입니다.

 

💭 들어가며

Spring 미션을 진행하면서 테스트에 대한 방향을 잡지 못해 초기에는 Layered Architecture의 각 계층에 대해 모두 테스트를 작성했다. 이후 테스트의 격리성에 대한 필요를 느끼며, Service 계층에서 Repository 의존성을 분리하기 위해 테스트 더블(Test Double)을 도입했고, 주로 Fake 객체를 활용했다. 이후 테스트 더블의 종류가 다양하다는 것을 알게 되어 이를 정리하였다.

 
 

✅ 테스트 더블이란

테스트 더블(Test Double)실제 객체를 대체하여 테스트에 사용하는 가짜 객체이다. 실제 객체를 사용하기 어렵거나 부적절한 상황에서 해당 역할을 흉내 내어 테스트를 보조한다. 이 개념은 영화 촬영에서 위험한 장면을 대신 수행하는 스턴트 더블에서 유래되었다.

 

 

✅ 테스트 더블을 사용하는 이유

  1. 테스트의 독립성을 보장한다.
    • 테스트 더블을 사용하면 DB, 네트워크, 파일 시스템 등 외부 리소스와의 의존성을 제거할 수 있다.
    • 이를 통해 외부 환경 변화와 무관하게 테스트 결과의 일관성을 유지할 수 있다.
  2. 테스트 실행 속도를 향상시킬 수 있다.
    • 실제 객체 대신 메모리 기반의 테스트 더블을 사용함으로써 I/O 지연 없이 빠르게 테스트를 실행할 수 있다.
  3. 아직 개발되지 않은 구성요소에 대한 테스트가 가능하다.
    • 개발 중이거나 외부 시스템 등 접근이 어려운 의존성 대신 테스트 더블을 사용하면, 해당 구성요소가 없어도 테스트를 먼저 진행할 수 있다.
  4. 특정 시나리오를 유도하거나 제어할 수 있다.
    • 실제 시스템에서 재현하기 어려운 예외 상황이나 동작 조건을 테스트 더블로 인위적으로 구성할 수 있다.
    • 예시
      • 특정 시간이 지난 후에만 동작하는 로직 -> 시간을 주입하는 객체를 더블로 대체
      • 랜덤 값을 반환하는 로직 -> 항상 동일한 값을 반환
  5. 상태나 동작에 대한 정밀한 관찰이 가능하다.
    • 특정 메서드 호출 여부, 호출 횟수, 전달된 인자 등을 검증함으로써 내부 동작을 정확하게 테스트할 수 있다.
    • 예시
      • 이메일 전송 여부 확인 -> 실제 메일 발송 없이 메서드 호출 검증
      • 주문 완료 시 포인트 차감 확인 -> 포인트 서비스의 메서드 호출 검증

 

 

✅ 테스트 더블의 종류

테스트 더블은 크게 Dummy, Fake, Stub, Mock, Spy로 나뉜다.

 

▶ Dummy

사용되지 않지만 자리를 채우는 용도
  • 가장 단순한 테스트 더블이다.
  • 객체는 필요하지만 실제 기능은 필요하지 않을 때 사용된다.
  • 메서드가 호출되면 정상 동작을 보장하지 않는다.
  • 주로 파라미터 전달을 위한 자리 채우기 용도로 사용된다.

🔽 예시 코드

class DummyLogger implements Logger {
    @Override
    public void log(String message) {
        // 아무것도 안 함
    }
}
class DummyTest {
    @Test
    void 로그를_사용하지_않는_로직() {
        DummyLogger dummyLogger = new DummyLogger();
        ReservationService service = new ReservationService(dummyLogger);

        // Logger는 호출되지 않음
        service.executeLogic(); 
    }
}

 

▶ Fake

간단한 동작을 흉내내는 실제 구현체
  • 실제 객체처럼 동작하지만 단순화된 로직을 가진 테스트 더블이다.
  • 실제 프로덕션 환경에는 적합하지 않다.
  • 인메모리 DB 등 테스트 전용 구현체로 사용된다.

🔽 예시 코드

public class FakeReservationRepository implements ReservationRepository {
    private final List<Reservation> reservations = new ArrayList<>();

    @Override
    public void save(Reservation reservation) {
        reservations.add(reservation);
    }
    @Override
    public List<Reservation> findAll() {
        return new ArrayList<>(reservations);
    }

    @Override
    public Optional<Reservation> findById(Long id) {
        return reservations.stream()
                .filter(reservation -> reservation.getId().equals(id))
                .findFirst();
    }
}
public class FakeTest {
    @Test
    void 중복_예약이면_예외() {
        ReservationRepository repository = new FakeReservationRepository();
        ReservationService service = new ReservationService(repository);

        service.reserve(new Reservation("미소", LocalDate.now(), 1L));

        assertThatThrownBy(() -> service.reserve(new Reservation("미소", LocalDate.now(), 1L)))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

 

▶ Stub

항상 고정된 결과 반환
  • 특정 메서드가 고정된 값을 반환하도록 설정된 테스트 더블이다.
  • 내부 상태나 호출 여부는 검증하지 않는다.
  • 실제 동작은 없지만, Dummy보다 한 단계 발전된 형태로, 마치 실제 객체처럼 동작하는 것처럼 보이게 구성된다.
  • 주로 반환값 확인에 초점을 둔다.

🔽 예시 코드

@ExtendWith(MockitoExtension.class)
class StubTest {

    @Mock
    ReservationRepository stubRepository;

    @InjectMocks
    ReservationService service;

    @Test
    void 예약_존재_시_예외_발생() {
        given(stubRepository.existsByDateAndTime(any(), anyLong())).willReturn(true);

        assertThatThrownBy(() -> service.reserve(new Reservation("미소", LocalDate.now(), 1L)))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

 

▶ Mock

행위 자체를 검증하는 객체
  • 호출에 대한 기대를 사전에 명세하고, 그 기대에 따라 테스트를 검증하도록 구성된 테스트 더블이다.
  • 메서드가 호출 여부, 횟수, 인자 등을 사전에 정의하고 검증한다.
  • 주된 목적은 행위 검증이며, 반환값보다는 객체 간 상호작용의 정확성을 테스트한다.
  • 대부분의 Mock 프레임워크는 Stub 기능도 포함하고 있어, Stub과 Mock의 경계가 모호할 수 있다.

🔽 예시 코드

@ExtendWith(MockitoExtension.class)
class MockTest {

    @Mock
    Logger mockLogger;

    @InjectMocks
    ReservationService service;

    @Test
    void 로직_수행_후_로그를_남긴다() {
        service.executeLogic();

        verify(mockLogger).log("예약 완료"); // "예약 완료" 인자, 메서드 한 번 호출 검증 (행위 검증)
    }
}

 

▶ Spy

실제 객체 + 일부만 Stub/Mock
  • 실제 객체처럼 동작하며 동시에 행위 검증도 가능한 하이브리드 테스트 더블이다.
  • 일부 메서드만 Stub화할 수 있다.
  • 실제 로직을 유지하면서 테스트 목적에 따라 행위 검증 가능하다.

🔽 예시 코드

@ExtendWith(MockitoExtension.class)
class SpyTest {

    @Spy
    List<String> spyList = new ArrayList<>();

    @Test
    void 실제_객체처럼_동작하며_행위_검증_가능() {
        spyList.add("미소");

        verify(spyList).add("미소"); // "미소" 인자, 메서드 한 번 호출 검증 (행위 검증)
        assertThat(spyList).hasSize(1); // 실제 리스트에 추가 여부 검증 (실제 객체처럼 동작)
    }
}

 

 

✅ 요약

종류 실제 동작 행위 검증 목적
Dummy X X 자리를 채우기 위한 객체
Fake X 단순한 로직으로 실제 동작을 흉내 냄
Stub △ (정해진 응답만) X 특정 메서드의 반환값 지정
Mock X O 메서드 호출 여부 및 인자 검증
Spy O O 실제 동작 + 호출 여부 및 인자 검증 가능
실제 동작 검사: 테스트 대상 메서드 실행 후, 객체의 상태(반환값, 필드 값, 저장된 객체 등)를 검사하는 것
행위 검증: 테스트 대상 메서드 실행 중/후, 특정 메서드가 어떻게 호출되었는지 (몇 번, 어떤 인자로 등) 검증하는 것

 

 

📍 참고 자료