Test Double

소프트웨어 테스트에서 실제 객체를 대신하여 사용되는 테스트용 객체를 말합니다.
이것은 마치 영화에서 위험한 장면을 연기하는 스턴트 더블과 비슷한 개념이다.
실제 구현체를 사용하기 어렵거나 비용이 많이 드는 상황에서 테스트를 용이하게 만들어주는 중요한 기법.

목적:

  1. 테스트 대상 코드를 외부 요인으로부터 격리
  2. 테스트 속도 개선
  3. 예측 불가능한 요소 제거
  4. 특정 상황 시뮬레이션
  5. 감춰진 정보 획득

장점:

  1. 외부 의존성 제거로 인한 테스트의 안정성 향상
  2. 테스트 실행 속도 개선
  3. 특정 시나리오 테스트 용이성 증가
  4. 아직 개발되지 않은 컴포넌트의 동작 시뮬레이션 가능

주의사항:

  1. 실제 객체와의 차이로 인한 오류 가능성
  2. 과도한 사용 시 테스트 코드의 복잡성 증가
  3. 실제 환경과의 차이로 인한 테스트 신뢰성 저하 가능성

테스트 더블의 종류

테스트 더블은 주로 다음 5가지 유형으로 분류된다.

더미 객체(Dummy Objects)

단순히 인스턴스화된 객체로, 기능은 필요하지 않을 때 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EmailService:
    def send_email(self, email, message):
        # 실제로는 이메일을 보내는 복잡한 로직
        pass

# 더미 객체
class DummyEmailService:
    def send_email(self, email, message):
        # 아무 동작도 하지 않음
        pass

class User:
    def __init__(self, email_service):
        self.email_service = email_service
        
    def welcome_user(self, email):
        self.email_service.send_email(email, "Welcome!")

# 테스트 코드
def test_user_creation():
    dummy_email = DummyEmailService()
    user = User(dummy_email)
    # 테스트 로직…

가짜 객체(Fake Objects)

실제 구현을 단순화한 객체로, 주로 데이터 접근 계층을 대체할 때 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class UserRepository:
    def save(self, user):
        # 실제 데이터베이스에 저장
        pass
    
    def find_by_id(self, user_id):
        # 데이터베이스에서 검색
        pass

# 가짜 객체
class FakeUserRepository:
    def __init__(self):
        self.users = {}  # 메모리 내 저장소
        
    def save(self, user):
        self.users[user.id] = user
        
    def find_by_id(self, user_id):
        return self.users.get(user_id)

# 테스트 코드
def test_user_repository():
    repo = FakeUserRepository()
    user = User(1, "John")
    repo.save(user)
    assert repo.find_by_id(1).name == "John"

스텁(Stubs)

미리 정의된 응답을 제공하는 객체로, 특정 상태를 시뮬레이션할 때 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class WeatherService:
    def get_temperature(self, city):
        # 실제로는 외부 API 호출
        pass

# 스텁
class WeatherServiceStub:
    def get_temperature(self, city):
        # 항상 동일한 값 반환
        return 25

class WeatherAlert:
    def __init__(self, weather_service):
        self.weather_service = weather_service
        
    def should_warn(self, city):
        temp = self.weather_service.get_temperature(city)
        return temp > 30

# 테스트 코드
def test_weather_alert():
    stub = WeatherServiceStub()
    alert = WeatherAlert(stub)
    assert not alert.should_warn("Seoul")

스파이(Spies)

실제 객체의 기능을 유지하면서 추가적인 정보를 기록하는 객체.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class LoggerSpy:
    def __init__(self):
        self.log_count = 0
        self.last_message = None
        
    def log(self, message):
        self.log_count += 1
        self.last_message = message

class UserService:
    def __init__(self, logger):
        self.logger = logger
        
    def create_user(self, name):
        self.logger.log(f"Creating user: {name}")
        # 사용자 생성 로직…

# 테스트 코드
def test_user_creation_logging():
    logger_spy = LoggerSpy()
    service = UserService(logger_spy)
    service.create_user("John")
    
    assert logger_spy.log_count == 1
    assert logger_spy.last_message == "Creating user: John"

목(Mocks)

예상되는 호출과 그에 대한 응답을 미리 프로그래밍한 객체로, 행위 검증에 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from unittest.mock import Mock

class PaymentGateway:
    def process_payment(self, amount):
        # 실제 결제 처리
        pass

class OrderService:
    def __init__(self, payment_gateway):
        self.payment_gateway = payment_gateway
        
    def place_order(self, amount):
        self.payment_gateway.process_payment(amount)
        return "Order placed"

# 테스트 코드
def test_order_placement():
    mock_gateway = Mock()
    mock_gateway.process_payment.return_value = True
    
    service = OrderService(mock_gateway)
    result = service.place_order(100)
    
    # 메서드가 정확한 인자와 함께 호출되었는지 검증
    mock_gateway.process_payment.assert_called_with(100)
    assert result == "Order placed"

Test Dobule 기법의 비교

특성Dummy ObjectsStubsFakesSpiesMocks
주요 목적파라미터 채우기미리 준비된 응답 제공실제 구현의 단순화호출 기록 및 검증예상 동작 검증
동작 방식아무 동작 없음하드코딩된 응답 반환실제와 유사하게 동작호출 정보 기록기대 동작 프로그래밍
구현 복잡도매우 낮음낮음중간높음매우 높음
행위 검증불가능제한적가능상세 가능매우 상세 가능
상태 검증불가능가능가능가능가능
실제 로직 포함없음최소한단순화된 형태선택적선택적
적합한 사용 사례미사용 의존성간단한 입출력복잡한 의존성호출 추적 필요정확한 동작 검증
설정 난이도매우 쉬움쉬움보통어려움매우 어려움
유지보수 비용매우 낮음낮음중간높음매우 높음
테스트 취약성매우 낮음낮음중간높음매우 높음

각 기법의 세부적인 특징:

  1. Dummy Objects:

    • 가장 단순한 형태의 Test Double
    • 실제로 사용되지 않는 파라미터를 위한 자리 표시자
    • 어떤 동작도 수행하지 않음
    • 실제 메서드가 호출되면 안 됨
  2. Stubs:

    • 미리 정의된 응답을 제공
    • 테스트 시나리오에 필요한 상태를 하드코딩
    • 단순한 조건부 동작 가능
    • 입력에 따른 다른 응답 제공 가능
  3. Fakes:

    • 실제 구현의 단순화된 버전
    • 동작하는 구현을 포함
    • 실제 객체와 유사하게 동작
    • 메모리 내 구현으로 성능 향상
  4. Spies:

    • 메서드 호출을 기록하고 추적
    • 호출 횟수, 파라미터, 순서 등을 기록
    • 실제 구현과 함께 사용 가능
    • 상세한 호출 정보 제공
  5. Mocks:

    • 가장 복잡하고 강력한 Test Double
    • 기대하는 동작을 미리 프로그래밍
    • 상호작용 검증에 중점
    • 매우 구체적인 행위 검증 가능

선택 기준:

  1. 단순한 의존성 처리만 필요한 경우:

    • Dummy Objects 사용
  2. 특정 응답만 필요한 경우:

    • Stubs 사용
  3. 복잡한 동작의 단순화가 필요한 경우:

    • Fakes 사용
  4. 메서드 호출 추적이 필요한 경우:

    • Spies 사용
  5. 정확한 상호작용 검증이 필요한 경우:

    • Mocks 사용

이러한 기법들의 효과적인 사용을 위해서는 다음 사항들을 고려해야 한다:

  1. 테스트의 목적과 범위
  2. 구현의 복잡성
  3. 유지보수 비용
  4. 테스트 실행 속도
  5. 테스트의 가독성과 이해도

참고 및 출처