소프트웨어 개발은 거대한 교향곡을 연주하는 오케스트라와 같습니다. 바이올린, 첼로, 트럼펫 등 각 파트(모듈)의 연주자들이 각자의 악보를 완벽하게 연주하는 것도 중요하지만, 결국 이 모든 소리가 조화롭게 어우러져야 비로소 아름다운 음악(소프트웨어)이 완성됩니다. 하지만 만약 1악장 연주를 시작해야 하는데, 첼로 파트 연주자들이 아직 도착하지 않았다면 어떨까요? 다른 연주자들은 연습을 멈추고 마냥 기다려야만 할까요?
소프트웨어 통합 테스트(Integration Test) 과정에서도 이와 비슷한 딜레마가 발생합니다. 내가 만든 모듈 A가 다른 동료가 만들고 있는 모듈 B를 호출해서 사용해야 하는데, 모듈 B가 아직 완성되지 않은 상황입니다. 이 경우, 모듈 A가 모듈 B를 올바르게 호출하는지, 그리고 모듈 B로부터 예상된 값을 돌려받았을 때 제대로 동작하는지 테스트할 방법이 없습니다. 바로 이 ‘아직 오지 않은 동료’의 빈자리를 임시로 채워주는 대역 연주자들이 바로 ‘테스트 스텁(Test Stub)’과 ‘테스트 드라이버(Test Driver)’입니다.
스텁과 드라이버는 테스트 대상 모듈이 독립적으로 테스트될 수 있도록 필요한 가상 환경을 만들어주는 ‘테스트 하네스(Test Harness)’의 핵심 구성 요소입니다. 이 둘은 서로 반대의 역할을 수행하며, 점진적인 통합 테스트를 가능하게 하는 필수적인 존재입니다. 본 글에서는 스텁과 드라이버가 각각 무엇이며, 언제, 어떻게 사용되는지 그 명확한 차이와 활용법을 구체적인 예시를 통해 완벽하게 이해해 보겠습니다.
테스트 스텁 (Test Stub): 하위 모듈을 대신하는 ‘대역 배우’
핵심 개념: 호출에 응답만 하는 가짜 하위 모듈
테스트 스텁은 아직 개발되지 않았거나, 테스트 환경에서 직접 사용하기 어려운 하위 모듈의 역할을 임시로 대신하는 ‘가짜’ 모듈입니다. 스텁의 역할은 매우 단순합니다. 상위 모듈로부터 호출을 받았을 때, 미리 약속된 고정된 값을 반환해 주는 것입니다. 이는 마치 영화 촬영 현장에서 위험한 액션 씬을 촬영할 때, 주연 배우를 대신하여 정해진 동선과 동작만 수행하는 ‘스턴트 대역 배우’와 같습니다.
스텁은 주로 시스템의 상위 모듈부터 개발하며 아래로 내려가는 ‘하향식 통합 테스트(Top-down Integration Testing)’에서 사용됩니다. 상위 모듈 A가 하위 모듈 B의 get_user_data() 함수를 호출해야 하는데, 모듈 B가 아직 개발 중이라고 가정해 봅시다. 이 상황에서 상위 모듈 A의 로직(예: 사용자 데이터를 받아와 화면에 이름을 표시하는 기능)을 테스트하기 위해, 우리는 ‘가짜’ get_user_data() 함수를 만듭니다.
이 가짜 함수, 즉 스텁은 내부에 데이터베이스를 조회하는 복잡한 로직 없이, 단순히 “호출되면 무조건 ‘홍길동’이라는 이름과 ’30세’라는 나이를 반환해라”라고 프로그래밍되어 있습니다. 이제 상위 모듈 A는 실제 모듈 B가 없더라도, 이 스텁을 호출하여 마치 실제 데이터를 받아온 것처럼 자신의 로직을 테스트할 수 있게 됩니다.
스텁의 주요 특징:
- 하위 모듈을 대체: 테스트 대상 모듈이 ‘호출하는(calls)’ 대상입니다.
- 수동적인 역할: 상위 모듈로부터 호출을 ‘당하는’ 입장에서, 정해진 값을 반환할 뿐입니다.
- 상태 검증에 사용: 스텁이 반환한 값을 받은 상위 모듈의 상태가 올바르게 변하는지를 검증하는 데 목적이 있습니다.
- 주요 사용처: 하향식 통합 테스트, 외부 API 연동 부 테스트.
적용 사례: 날씨 앱의 UI 모듈 테스트
‘오늘의 날씨’를 화면에 표시해주는 모바일 앱을 개발한다고 상상해 봅시다. 화면을 담당하는 WeatherUI 모듈과, 실제 기상청 서버와 통신하여 날씨 데이터를 가져오는 WeatherAPI 모듈로 구성되어 있습니다. 하향식 접근법에 따라, 개발자는 먼저 WeatherUI 모듈부터 개발을 완료했습니다.
테스트 대상: WeatherUI 모듈. 이 모듈은 WeatherAPI.getCurrentWeather() 함수를 호출하여 날씨 정보를 받아온 뒤, 화면의 온도 텍스트와 날씨 아이콘을 업데이트하는 displayWeather() 함수를 가지고 있습니다.
문제 상황: WeatherAPI 모듈은 아직 개발 중이라 실제 날씨 데이터를 가져올 수 없습니다.
이때 QA 엔지니어는 WeatherAPI 모듈을 흉내 내는 테스트 스텁을 작성합니다.
WeatherAPI 스텁 코드 (Python 예시):
Python
class WeatherAPI_Stub:
def getCurrentWeather(self, city):
# 실제 API 호출 로직은 없음
# 어떤 도시가 입력되든 항상 미리 정해진 가짜 데이터를 반환
print(f"[STUB] '{city}' 날씨 요청을 받았습니다. 고정된 값을 반환합니다.")
fake_weather_data = {"temperature": 25, "condition": "맑음"}
return fake_weather_data
이제 WeatherUI 모듈을 테스트하는 코드는 실제 WeatherAPI 대신 이 WeatherAPI_Stub을 사용합니다.
WeatherUI 테스트 코드:
Python
def test_displayWeather_with_stub():
# 1. 테스트 환경 설정
ui_module = WeatherUI()
api_stub = WeatherAPI_Stub() # 실제 객체 대신 스텁 객체 사용
# 2. 테스트할 기능 실행
# ui_module은 내부적으로 api_stub.getCurrentWeather("서울")을 호출할 것임
ui_module.displayWeather("서울", api_stub)
# 3. 결과 검증
# ui_module의 화면 온도가 스텁이 반환한 25도로 설정되었는지 확인
assert ui_module.getTemperatureText() == "25°C"
# ui_module의 날씨 아이콘이 '맑음' 아이콘으로 설정되었는지 확인
assert ui_module.getWeatherIcon() == "sun_icon.png"
이 테스트를 통해, 우리는 WeatherAPI 모듈의 개발 완료 여부와 상관없이 WeatherUI 모듈이 날씨 데이터를 받아 화면에 올바르게 표시하는 핵심 로직을 완벽하게 검증할 수 있습니다. 스텁 덕분에 우리는 외부 의존성(기상청 서버의 상태, 네트워크 등)으로부터 완전히 독립된 안정적인 테스트 환경을 구축한 것입니다.
테스트 드라이버 (Test Driver): 상위 모듈을 대신하는 ‘임시 조종사’
핵심 개념: 테스트 대상을 호출하고 제어하는 가짜 상위 모듈
테스트 드라이버는 스텁과 정확히 반대되는 역할을 합니다. 아직 개발되지 않은 상위 모듈을 대신하여, 테스트 대상이 되는 하위 모듈을 ‘호출’하고, 테스트 데이터를 ‘입력’하며, 그 결과를 받아 ‘검증’하는 임시 코드 또는 도구입니다. 드라이버는 마치 자동차를 테스트하기 위해 임시로 만든 ‘운전석’과 ‘계기판’처럼, 테스트 대상 모듈을 조종하고 상태를 확인하는 역할을 합니다.
드라이버는 주로 시스템의 하위 모듈부터 개발하며 위로 올라가는 ‘상향식 통합 테스트(Bottom-up Integration Testing)’에서 사용됩니다. 앞선 예시와 반대로, WeatherAPI 모듈의 개발이 먼저 끝났다고 가정해 봅시다. 이 모듈은 기상청 서버와 통신하여 날씨 데이터를 가져오는 복잡한 로직을 담고 있습니다. 하지만 이 모듈을 호출하여 사용할 상위 모듈인 WeatherUI는 아직 개발되지 않았습니다.
이 경우, 우리는 WeatherAPI 모듈이 과연 올바르게 서버와 통신하고, 데이터를 정확하게 파싱하여 반환하는지 테스트할 방법이 없습니다. 이때 우리는 WeatherAPI 모듈을 테스트하기 위한 ‘테스트 드라이버’를 작성합니다.
이 드라이버는 다음과 같은 일을 수행하는 간단한 프로그램입니다.
- 테스트 대상인
WeatherAPI객체를 생성합니다. WeatherAPI.getCurrentWeather()함수를 ‘서울’이라는 테스트 데이터와 함께 호출합니다.- 함수로부터 반환된 날씨 데이터 객체를 받습니다.
- 반환된 객체의 온도 값이 숫자인지, 날씨 상태 값이 ‘맑음’, ‘흐림’ 등 예상된 문자열 중 하나인지 등을 검증합니다.
- 테스트 결과를 콘솔에 출력합니다.
드라이버의 주요 특징:
- 상위 모듈을 대체: 테스트 대상 모듈을 ‘호출하는(calls)’ 주체입니다.
- 능동적인 역할: 테스트의 시작과 흐름을 ‘주도’합니다.
- 결과 검증에 사용: 테스트 대상 모듈이 반환한 결과가 올바른지 직접 검증합니다.
- 주요 사용처: 상향식 통합 테스트, 핵심 비즈니스 로직/알고리즘 모듈 테스트.
적용 사례: 환율 계산 모듈 테스트
외부 금융 정보 API를 호출하여 실시간 환율 정보를 가져오고, 특정 금액을 환전했을 때의 결과를 계산해주는 ExchangeRateCalculator 모듈을 개발했다고 가정해 봅시다. 이 모듈은 상위의 어떤 UI에서도 호출될 수 있는 공용 컴포넌트입니다. 아직 이 모듈을 사용하는 상위 모듈이 없으므로, 우리는 테스트 드라이버를 만들어 이 모듈의 정확성을 검증해야 합니다.
테스트 대상:ExchangeRateCalculator 모듈. 이 모듈은 convert(amount, from_currency, to_currency) 라는 핵심 함수를 가지고 있습니다.
ExchangeRateCalculator를 위한 테스트 드라이버 코드 (Python 예시):
Python
# 테스트 드라이버 역할을 하는 메인 스크립트
def main_driver():
print("환율 계산 모듈 테스트를 시작합니다.")
# 1. 테스트 환경 설정
calculator = ExchangeRateCalculator()
# --- 테스트 케이스 1: 100달러를 원화로 환전 ---
print("\n[TC-001] USD to KRW 테스트")
amount_usd = 100
expected_result_range = (130000, 140000) # 환율은 변동하므로 범위로 검증
# 2. 테스트할 기능 실행 (모듈 호출)
result_krw = calculator.convert(amount_usd, "USD", "KRW")
# 3. 결과 검증
print(f" -> 결과: {result_krw} KRW")
if expected_result_range[0] <= result_krw <= expected_result_range[1]:
print(" -> TC-001: PASS")
else:
print(" -> TC-001: FAIL")
# --- (다른 통화에 대한 추가적인 테스트 케이스들) ---
if __name__ == "__main__":
main_driver()
이 main_driver() 함수가 포함된 파이썬 스크립트가 바로 테스트 드라이버입니다. 우리는 이 스크립트를 직접 실행함으로써, 아직 UI가 없더라도 ExchangeRateCalculator 모듈의 핵심 기능이 올바르게 동작하는지 독립적으로, 그리고 철저하게 테스트할 수 있습니다. 상향식 테스트 방식에서는 이처럼 가장 근간이 되는 하위 모듈들의 품질을 드라이버를 통해 완벽하게 확보한 뒤, 점차 상위 모듈과 통합해 나가는 방식으로 안정성을 높입니다.
마무리: 서로를 보완하는 테스트의 단짝
지금까지 살펴본 것처럼, 테스트 스텁과 드라이버는 통합 테스트라는 큰 그림 안에서 서로를 보완하는 완벽한 한 쌍입니다. 이 둘의 핵심적인 차이를 다시 한번 정리해 보겠습니다.
| 구분 | 테스트 스텁 (Test Stub) | 테스트 드라이버 (Test Driver) |
| 대체 대상 | 하위 모듈 | 상위 모듈 |
| 역할 | 호출에 응답하는 가짜 모듈 | 테스트 대상을 호출하고 제어하는 임시 모듈 |
| 주요 목적 | 상위 모듈의 로직 검증 | 하위 모듈의 로직 검증 |
| 사용 전략 | 하향식 통합 테스트 | 상향식 통합 테스트 |
| 동작 방식 | 수동적 (호출을 기다림) | 능동적 (테스트를 주도함) |
| 비유 | 대역 배우, 스턴트맨 | 임시 운전사, 조종 장치 |
어떤 모듈을 개발하든, 그 모듈은 다른 모듈을 호출하거나(하위 모듈에 의존), 다른 모듈에게 호출될(상위 모듈에 의존) 수밖에 없습니다. 스텁과 드라이버는 이러한 ‘의존성’의 고리를 임시로 끊어내어, 우리가 테스트하고 싶은 대상에만 온전히 집중할 수 있도록 도와주는 강력한 도구입니다.
스텁과 드라이버를 효과적으로 활용함으로써 우리는 전체 시스템이 완성될 때까지 막연히 기다리는 대신, 개발이 완료되는 부분부터 점진적으로, 그리고 체계적으로 품질을 검증해 나갈 수 있습니다. 이는 결국 전체 개발 과정의 불확실성을 줄이고, 프로젝트 후반부에 발생할 수 있는 치명적인 통합 오류를 사전에 방지하여, 더 견고하고 신뢰성 있는 소프트웨어를 만드는 가장 현명한 방법입니다.
