IT Professional Engineering/SW

테스트 스텁(Test Stub): 모듈 간 의존성 문제 해결을 위한 테스트 더블 기법

GilliLab IT 2025. 3. 27. 22:45
728x90
반응형

테스트 스텁(Test Stub): 모듈 간 의존성 문제 해결을 위한 테스트 더블 기법

테스트 스텁의 개념

  • 정의: 테스트 대상 모듈이 호출하는 다른 소프트웨어 구성 요소(모듈, 변수, 객체)를 일시적으로 대체하는 소프트웨어 구성 요소
  • 목적: 복잡한 의존성을 단순화하여 격리된 환경에서 테스트 진행 가능
  • 테스트 더블(Test Double)의 한 유형: 테스트 더블은 실제 객체 대신 테스트에서 사용되는 모든 종류의 가짜 객체를 포괄하는 용어
  • 사용 시점: 의존하는 컴포넌트가 아직 개발되지 않았거나, 실행하기 어려운 경우, 실행 시간이 오래 걸리는 경우에 활용

테스트 스텁의 필요성

  • 모듈 간 의존성 문제 해결: 모듈 A가 모듈 B에 의존할 때, B가 없어도 A를 테스트 가능
  • 테스트 격리(Isolation): 테스트 대상 코드만 검증 가능한 환경 조성
  • 제어 불가능한 요소 제어: 네트워크, 데이터베이스, 외부 API 등 통제하기 어려운 요소를 제어 가능한 형태로 대체
  • 예측 가능한 테스트 결과: 일관된 테스트 결과를 얻을 수 있어 테스트 신뢰성 향상
  • 테스트 속도 개선: 복잡한 연산이나 I/O 작업을 단순화하여 테스트 실행 속도 향상

테스트 스텁과 다른 테스트 더블의 비교

graph TD
    A[테스트 더블] --> B[테스트 스텁]
    A --> C[테스트 스파이]
    A --> D[테스트 페이크]
    A --> E[테스트 목]
    A --> F[테스트 더미]

    B --> B1[입력에 대한 미리 정의된 응답 반환]
    C --> C1[호출 여부, 방법 기록]
    D --> D1[실제 구현의 간소화된 버전]
    E --> E1[예상되는 호출 검증]
    F --> F1[전달만 되고 사용되지 않는 객체]
  • 테스트 스텁: 호출에 대한 하드코딩된 응답 제공, 행동 검증 없음
  • 테스트 목(Mock): 예상되는 호출을 검증하고 미리 프로그래밍된 응답 제공
  • 테스트 스파이(Spy): 실제 객체를 사용하되 호출 정보를 기록
  • 테스트 페이크(Fake): 실제 구현의 경량화된 버전(예: 인메모리 DB)
  • 테스트 더미(Dummy): 전달용으로만 사용되고 실제로는 사용되지 않는 객체

테스트 스텁 구현 방법

1. 수동 구현 방식

// 원래 의존하는 실제 서비스 인터페이스
public interface PaymentGateway {
    boolean processPayment(double amount);
}

// 테스트를 위한 스텁 구현
public class PaymentGatewayStub implements PaymentGateway {
    @Override
    public boolean processPayment(double amount) {
        // 항상 성공 반환 (실제 지불 처리 로직 대체)
        return true;
    }
}

// 테스트 코드
@Test
public void testOrderProcessWithPayment() {
    // 스텁 객체 생성
    PaymentGateway stubGateway = new PaymentGatewayStub();

    // 테스트 대상 객체에 스텁 주입
    OrderProcessor processor = new OrderProcessor(stubGateway);

    // 테스트 실행
    boolean result = processor.processOrder(new Order(100.0));

    // 결과 검증
    assertTrue(result);
}

2. 모킹 프레임워크 활용 (예: Mockito)

@Test
public void testOrderProcessWithMockito() {
    // Mockito를 사용한 스텁 생성
    PaymentGateway stubGateway = Mockito.mock(PaymentGateway.class);

    // 스텁 동작 정의
    Mockito.when(stubGateway.processPayment(Mockito.anyDouble())).thenReturn(true);

    // 테스트 대상 객체에 스텁 주입
    OrderProcessor processor = new OrderProcessor(stubGateway);

    // 테스트 실행
    boolean result = processor.processOrder(new Order(100.0));

    // 결과 검증
    assertTrue(result);
}

실제 사례: 데이터베이스 연동 테스트

문제 상황

  • 사용자 서비스 모듈이 데이터베이스 접근 계층에 의존
  • 테스트 시 실제 DB 연결은 느리고 환경 의존적

스텁 적용 전 코드

public class UserService {
    private UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public User findUserById(long id) {
        return repository.findById(id);
    }

    public boolean isUserPremium(long id) {
        User user = repository.findById(id);
        return user != null && user.isPremium();
    }
}

// 실제 DB에 접근하는 리포지토리
public class UserRepositoryImpl implements UserRepository {
    @Override
    public User findById(long id) {
        // 실제 데이터베이스에서 사용자 조회
        Connection conn = DatabaseConnection.getConnection();
        // SQL 실행 및 결과 매핑...
        return user;
    }
}

스텁 적용 후 테스트 코드

@Test
public void testIsUserPremium_WithPremiumUser() {
    // 테스트용 스텁 생성
    UserRepository stubRepository = new UserRepositoryStub();

    // 서비스에 스텁 주입
    UserService service = new UserService(stubRepository);

    // 테스트 실행
    boolean isPremium = service.isUserPremium(1L);

    // 결과 검증
    assertTrue(isPremium);
}

// 스텁 구현
class UserRepositoryStub implements UserRepository {
    @Override
    public User findById(long id) {
        // 항상 프리미엄 사용자 반환
        User stubUser = new User(id, "Test User", true);
        return stubUser;
    }
}

테스트 스텁 설계 원칙

  • 단순성: 가능한 간단하게 설계하여 테스트 자체의 복잡도 증가 방지
  • 일관성: 동일한 입력에 항상 동일한 결과 반환
  • 관련성: 테스트 목적과 관련된 응답만 구현
  • 격리성: 실제 의존성으로부터 테스트 대상 완전 격리
  • 가독성: 다른 개발자가 이해하기 쉽게 작성

테스트 스텁의 장단점

장점

  • 테스트 속도 향상: 복잡한 처리를 단순화하여 테스트 속도 개선
  • 결정적(Deterministic) 테스트: 일관된 결과 보장으로 테스트 안정성 증가
  • 의존성 단순화: 복잡한 외부 의존성 없이 테스트 가능
  • 에지 케이스 테스트: 실제 환경에서 재현하기 어려운 상황 시뮬레이션 가능
  • 병렬 개발: 의존하는 컴포넌트가 완성되기 전에도 개발 및 테스트 진행 가능

단점

  • 실제 환경 차이: 스텁은 실제 구현체와 동작이 다를 수 있음
  • 유지보수 부담: 인터페이스 변경 시 스텁도 함께 수정 필요
  • 테스트 복잡성: 과도한 스텁 사용은 테스트 코드 복잡성 증가 초래
  • 통합 이슈 발견 어려움: 컴포넌트 간 실제 상호작용 문제 발견 어려움

테스트 스텁의 활용 시나리오

1. 외부 API 의존성 처리

  • 문제: 결제 처리 시 외부 결제 게이트웨이 API 호출 필요
  • 해결: 결제 API 스텁을 만들어 다양한 응답 시나리오 테스트
// 결제 API 스텁
public class PaymentGatewayStub implements PaymentGateway {
    private boolean shouldSucceed;

    public PaymentGatewayStub(boolean shouldSucceed) {
        this.shouldSucceed = shouldSucceed;
    }

    @Override
    public PaymentResult processPayment(PaymentRequest request) {
        if (shouldSucceed) {
            return new PaymentResult(true, "Payment successful", "TX123456");
        } else {
            return new PaymentResult(false, "Insufficient funds", null);
        }
    }
}

2. 시간 의존적 코드 테스트

  • 문제: 특정 시간에 따라 다른 결과를 반환하는 로직
  • 해결: 시간 제공자 인터페이스를 만들고 스텁으로 대체
public interface TimeProvider {
    LocalDateTime getCurrentTime();
}

// 테스트용 시간 제공자 스텁
public class FixedTimeProvider implements TimeProvider {
    private final LocalDateTime fixedTime;

    public FixedTimeProvider(LocalDateTime fixedTime) {
        this.fixedTime = fixedTime;
    }

    @Override
    public LocalDateTime getCurrentTime() {
        return fixedTime;
    }
}

// 테스트 코드
@Test
public void testBusinessHoursCheck() {
    // 업무 시간 내 고정 시간 스텁
    TimeProvider businessHoursStub = new FixedTimeProvider(
        LocalDateTime.of(2023, 5, 10, 14, 0)); // 오후 2시

    BusinessService service = new BusinessService(businessHoursStub);
    assertTrue(service.isDuringBusinessHours());

    // 업무 시간 외 고정 시간 스텁
    TimeProvider afterHoursStub = new FixedTimeProvider(
        LocalDateTime.of(2023, 5, 10, 22, 0)); // 오후 10시

    service = new BusinessService(afterHoursStub);
    assertFalse(service.isDuringBusinessHours());
}

테스트 스텁 구현 시 모범 사례

  1. 인터페이스 기반 설계: 의존성을 인터페이스로 추상화하여 스텁 교체 용이성 확보
  2. 의존성 주입 활용: 생성자/세터 주입으로 테스트 시 스텁 교체 용이성 제공
  3. 경계값 테스트: 정상 케이스뿐만 아니라 예외 상황도 스텁으로 시뮬레이션
  4. 스텁 재사용: 공통 스텁 라이브러리 구축으로 중복 코드 감소
  5. 상태 확인: 스텁 사용 후 검증은 상태(state) 기반으로 수행

마이크로서비스 환경에서의 테스트 스텁 활용

graph LR
    A[주문 서비스] --> B[결제 서비스]
    A --> C[재고 서비스]
    A --> D[배송 서비스]

    subgraph "테스트 환경"
    A --> B1[결제 서비스 스텁]
    A --> C1[재고 서비스 스텁]
    A --> D1[배송 서비스 스텁]
    end
  • 서비스 계약 테스트: 마이크로서비스 간 API 계약 준수 확인
  • 장애 시나리오 테스트: 다른 서비스 장애 상황 시뮬레이션
  • 성능 테스트: 의존 서비스 지연 없이 부하 테스트 수행
  • 개발 생산성: 다른 팀의 서비스 완성 전에도 개발 진행 가능

결론

  • 테스트 스텁은 복잡한 의존성을 단순화하여 테스트 환경을 격리하고, 예측 가능한 결과를 제공함으로써 테스트 신뢰성과 효율성을 향상시킨다.
  • 이를 통해 개발자는 실제 의존성에 구애받지 않고 독립적으로 테스트를 수행할 수 있다.
  • 테스트 스텁은 특히 외부 시스템과의 통합 테스트에서 유용하며, 개발 초기 단계에서 빠른 피드백을 제공한다.
  • 하지만 스텁의 구현이 실제 동작과 다를 경우 테스트 결과의 신뢰성이 떨어질 수 있으므로, 적절한 설계와 관리가 필요하다.

Keywords

Test Double, Stub, Unit Testing, 모듈 의존성, 테스트 대역, 격리 테스트, 소프트웨어 테스팅, 테스트 자동화, Mock Object

728x90
반응형