test

Test Double 개념과 Mock, Stub의 차이 이해하기

소프트웨어 개발 과정에서 테스트는 기능의 신뢰성을 확보하고, 변경에 안전한 구조를 유지하는 핵심 요소입니다. 그러나 테스트가 외부 시스템(DB, API, 메시지 브로커 등)에 직접 의존할 경우 다음과 같은 문제가 발생할 수 있습니다.

  • 테스트 실행 속도가 느려짐
  • 네트워크 환경, 외부 시스템 상태 등에 따라 테스트 결과가 달라짐
  • 테스트 환경 재현이 어렵고 유지 비용이 증가함

따라서 테스트에서는 가능한 한 외부 의존성을 제거하고, 내부 로직의 정확성에 집중하는 구조가 필요합니다. 이때 활용되는 기법이 Test Double입니다. Test Double은 실제 객체를 대신하여 테스트 흐름을 안정적으로 제어할 수 있는 대역 객체를 의미하며, 테스트 환경을 독립적이고 예측 가능하게 유지합니다. 이 방식은 테스트 속도를 높이고, 불필요한 환경 설정 비용을 줄이며, 코드 변경 시 영향 범위를 좁혀 유지보수성을 크게 개선합니다.


Test Double의 유형: Mock과 Stub

Test Double 중에서도 가장 자주 사용되는 것이 Mock과 Stub이며, 두 객체는 혼동되는 경우가 많지만 목적이 분명히 다릅니다.

구분MockStub
역할메서드 호출 방식 및 행동을 검증반환 값을 사전에 정의하여 상태를 통제
초점Behavior(행동)State(상태)
주요 목적호출 횟수, 파라미터, 호출 여부 확인시나리오에 필요한 특정 값을 반환하도록 설정
적합한 테스트로직 흐름 검증외부 의존성 제거 및 결과 기반 테스트

Mock를 사용해야하는 상황

  • 특정 메서드가 정확히 몇 번 호출되었는지 확인해야 하는 경우
  • 전달된 파라미터 값 검증이 중요한 경우
  • 올바른 시점에 올바른 호출이 이루어졌는지를 검증해야하는 경우

Stub를 사용해야하는 상황

  • 외부 시스템이나 I/O 의존성을 테스트 대상에서 분리하고 싶은 경우
  • 다양한 반환 케이스를 만들어 분기/예외 처리 로직을 검증해야 하는 경우
  • 결과 기반 테스트가 필요한 경우

Mock은 어떻게 호출되었는가를 검증하고, Stub은 무엇을 반환할 것인가를 제어합니다. Mock은 행동 중심(Behavior Verification), Stub은 상태 중심(State Verification)이라는 차이를 기준으로 구분하면 명확합니다.


Mock 예시 (TypeScript + Jest)

Mock은 메서드가 어떻게 호출되었는지를 확인하는 데 사용됩니다. 특정 로직이 의도한 대로 외부 의존 객체를 호출하고 있는지를 검증할 때 유용합니다.

// user.service.ts
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}
 
  registerUser(email: string, password: string) {
    const user = { email, password };
    this.userRepository.save(user);
  }
}
// userRepository interface
export interface UserRepository {
  save(user: { email: string; password: string }): void;
}
// user.service.spec.ts
import { UserService } from './user.service';
 
test('userRepository.save가 1회 호출되는지 검증', () => {
  const mockRepository = {
    save: jest.fn()
  };
 
  const userService = new UserService(mockRepository);
  userService.registerUser('test@example.com', 'password123');
 
  expect(mockRepository.save).toHaveBeenCalledTimes(1);
  expect(mockRepository.save).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'password123'
  });
});
  • jest.fn()으로 생성된 save는 실제 구현이 없는 Mock 함수입니다.
  • 테스트 목표는 저장 동작의 결과가 아니라, UserService가 userRepository.save()를 의도한 정확한 방식으로 호출했는가를 검증하는 것입니다.

Stub 예시 (TypeScript + Jest)

Stub은 특정 반환 값을 미리 정의하여 테스트 상황을 통제하는 용도로 사용합니다. 이를 통해 외부 시스템이나 데이터 저장 과정이 실제로 일어나지 않더라도, 테스트 대상 로직이 해당 반환 값을 기반으로 제대로 동작하는지를 검증할 수 있습니다.

// user.service.ts (수정)
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}
 
  registerUser(email: string, password: string) {
    const savedUser = this.userRepository.save({ email, password });
    return savedUser.id;
  }
}
// user.service.spec.ts
import { UserService } from './user.service';
 
test('userRepository.save가 미리 정의된 값을 반환하도록 Stub 설정', () => {
  const stubRepository = {
    save: jest.fn().mockReturnValue({ id: 1, email: 'hanpy@example.com', password: 'pw123' })
  };
 
  const userService = new UserService(stubRepository);
  const id = userService.registerUser('hanpy@example.com', 'pw123');
 
  expect(id).toBe(1);
});

Stub은 특정 반환 값을 미리 정의하여 테스트 상황을 통제하는 용도로 사용합니다. 이를 통해 외부 시스템이나 데이터 저장 과정이 실제로 일어나지 않더라도, 테스트 대상 로직이 해당 반환 값을 기반으로 제대로 동작하는지를 확인 할 수 있습니다.


추가 예시

Mock 활용 예시 (행동 검증)

test('결제 요청 메서드가 호출되었는지 검증', () => {
  const mockGateway = {
    charge: jest.fn().mockReturnValue(true)
  };
 
  const service = new PaymentService(mockGateway);
  const request = { amount: 1000 };
 
  service.processPayment(request);
 
  expect(mockGateway.charge).toHaveBeenCalledTimes(1);
  expect(mockGateway.charge).toHaveBeenCalledWith(request);
});

mockGateway.charge를 expect에 넣은 이유는, PaymentService가 외부 의존성을 어떻게 사용하는지(행동) 를 검증하기 위함 입니다. PaymentService 내부에 코드를 고민해보면 아래와 같을 겁니다.

class PaymentService {
  constructor(private gateway) {}
 
  processPayment(request) {
    // 핵심 로직: 외부 결제 게이트웨이를 호출함
    return this.gateway.charge(request);
  }
}

여기서 gateway.charge()는 외부 시스템(결제 서버 등) 을 호출하는 부분입니다. 테스트에서는 실제 결제 API를 호출하면 안 되니까, 그 자리를 Mock 객체로 대체한 거죠.

Stub 활용 예시 (반환 상태 검증)

test('결제 실패 상황을 Stub으로 시뮬레이션', () => {
  const stubGateway = {
    charge: jest.fn().mockReturnValue(false)
  };
 
  const service = new PaymentService(stubGateway);
  const request = { amount: 1000 };
 
  const result = service.processPayment(request);
 
  expect(result).toBe(false);
});
구분테스트 대상검증 포인트사용하는 도구
Mock 테스트PaymentService외부 객체(gateway) 호출 여부 / 횟수 / 인자jest.fn() + expect(...).toHaveBeenCalled...
Stub 테스트PaymentService외부 객체의 반환값에 따른 서비스 동작 결과jest.fn().mockReturnValue(...) + expect(result)

정리

항목MockStub
초점메서드 호출 행위 검증메서드 반환값 제어
목적로직 흐름 검증특정 상태 가정 테스트
장점테스트 시나리오 명확화예외 상황 테스트 용이

Test Double을 적절히 사용하면 테스트를 빠르게 수행할 수 있고 코드 구조도 느슨하게 유지할 수 있어 유지보수 효율이 크게 향상됩니다.


참고 자료