테스트하네스로 반복 가능한 자동화 테스트 환경 만들기
드라이버·스텁·테스트 데이터를 묶는 테스트하네스의 구조와 격리, 결정성, 정리 자동화 원칙을 실무 관점에서 정리한다.
2026-08-14 · 최초 발행 2025-12-15
테스트 실행을 둘러싼 코드와 데이터를 함께 관리한다
테스트하네스(Test Harness)는 시스템이나 컴포넌트를 검증하기 위해 의도적으로 마련한 코드와 데이터의 묶음이다. 테스트 프레임워크가 러너, DSL, 리포팅의 기반을 제공한다면, 하네스는 그 위에서 특정 시스템을 실제로 실행하기 위한 장치와 데이터, 배선 논리를 담당한다.
목표는 단순히 테스트를 실행하는 데 있지 않다. 드라이버와 스텁을 포함한 보조 코드, 테스트 데이터, 검증과 정리 절차를 일관되게 연결해 반복 가능하고 격리된 실행 흐름을 만드는 데 있다. 시스템 또는 컴포넌트 수준에서 테스트 데이터와 아티팩트는 실행 후 반드시 삭제 대상이 된다.
하네스에는 보통 다음 요소가 포함된다.
- 드라이버(Driver)
- 스텁·페이크·목(Stub/Fake/Mock)
- 픽스처와 시드 같은 테스트 데이터
- 오케스트레이션 스크립트와 환경 제어
- 어서션·오라클, 로그와 리포트 수집기
자동화가 흔들리지 않게 만드는 조건
테스트 환경은 동일한 버전의 바이너리, 환경변수, 시크릿 주입 상태를 유지해야 한다. 컨테이너나 가상화 기반 격리를 적용하고, 네트워크·파일·시간처럼 외부에서 달라질 수 있는 의존성을 제어 가능한 상태로 둔다. 이때 스텁, 가짜 시계, 가상 파일시스템이 쓰인다.
데이터는 최소하고 명시적이며 독립적이어야 한다. 식별자 충돌을 막기 위한 네임스페이스나 샌드박스를 마련하고, 생성(Arrange)→사용(Act)→삭제(Assert 후 Cleanup)의 수명 주기를 정한다. 테스트가 끝나면 데이터를 반드시 삭제한다.
난수, 시간, 동시성도 재현성을 좌우한다. 시드를 고정하고 가짜 시간(clock), 단일 스레드 모드 또는 고정 스케줄러를 이용해 결과가 달라지는 요인을 통제한다. 실패를 분석할 수 있도록 로그·메트릭·트레이싱을 연계하고, 실패 시에는 스크린샷, 요청/응답 페이로드, DB 다이제스트 같은 수집물을 확대할 수 있어야 한다.
CI/CD에서는 병렬 실행, 캐시, 아티팩트 업로드, 실패 격리 재시도(retry), 플래키 탐지 격리가 하네스의 실행 흐름에 연결된다.
드라이버부터 리포트까지 이어지는 역할
드라이버는 테스트 케이스를 실제 실행 단위로 바꾸는 제어 코드다. 입력 조합을 만들고 시나리오 순서를 관리하며 리포트를 출력한다. 에러, 타임아웃, 재시도 정책을 포함해 실패를 격리하고 부분 재실행을 지원할 수 있다.
외부 시스템이나 의존 모듈은 스텁·페이크·목으로 대체한다. 고정 응답, 조건부 응답, 상태를 가진 대체 구현을 두고 네트워크 가상화, 파일 I/O 대체, 시간 제어를 통해 결정성을 확보한다. 계약 테스트를 함께 사용하면 실제 인터페이스와의 일관성도 유지할 수 있다.
테스트 데이터 영역에서는 최소 데이터 세트와 빌더 패턴(생성자/팩토리)을 제공해 케이스별 Arrange를 명시한다. 트랜잭션 롤백, 임시 스키마 드롭, 네임스페이스 TTL 같은 삭제 메커니즘도 하네스에 포함된다.
환경 제어는 서비스 기동과 종료, 포트 할당, 컨테이너 생성과 파괴, 시크릿 주입을 자동화한다. 실패했을 때는 덤프 수집, 헬스체크, 진단 로그 패키지화가 필요하다. 검증 단계에서는 비즈니스 규칙 중심의 어서션과 골든 파일·스냅샷 기반의 시멘틱 비교를 수행하며, 케이스와 요구사항·버그 링크를 연결해 결과의 추적성을 확보한다.
드라이버와 대체 의존성이 만나는 흐름
외부 API와 배치, 데이터 흐름에 적용하는 방식
마이크로서비스에서는 결제·신용조회·알림 같은 외부 API를 스텁으로 대체할 수 있다. 계약 테스트로 인터페이스 호환성을 보장하면서 네트워크 불안정과 요금 비용을 제거하고 빠르게 회귀를 수행한다.
레거시 시스템의 CLI·배치 테스트에서는 드라이버가 프로세스 런처 역할을 맡는다. 입력 파일 생성→실행→출력 파싱→삭제를 자동화하고, 장애가 발생하면 로그 스냅샷과 리턴코드를 수집한다.
데이터 파이프라인은 샘플 데이터셋 시드→변환 실행→파케이/CSV 스냅샷 비교→스테이징 테이블 드롭 순서로 검증할 수 있다. 트랜잭션 격리나 임시 스키마 전략을 적용해 잔여 데이터를 남기지 않는다.
모바일·웹 UI 스모크 테스트에서는 디바이스 팜 또는 헤드리스 브라우저에 드라이버를 적용하고, 백엔드 호출은 스텁으로 처리한다. 스크린샷과 네트워크 로그는 아티팩트로 업로드한다.
회귀 테스트 시간을 바꾸는 실행 기반
회귀 1,000케이스를 케이스당 수동 5분으로 수행하면 총 5,000분(≈83.3시간)이 걸린다. 하네스 자동화 후 케이스당 2초, 워커 10개 병렬 실행이라면 (1,000×2)초/10=200초(≈3.3분)으로 계산된다. 이는 리드타임을 줄이고 개발자 피드백 루프를 빠르게 만든다.
결정성과 격리를 갖추면 플래키율을 낮추고 실패 재현성을 높여 디버깅 시간을 줄일 수 있다. 후속 테스트 이전에 정확한 품질 게이트를 둘 수 있으며, 복잡한 스크립트·경계조건·희귀 사례도 자동화 대상에 포함된다. 야간 일배치와 PR 게이트 통합도 가능하다.
외부 API 요금과 테스트 환경 유지비를 줄이고, 실패 원인 탐지에 드는 시간을 단축해 인건비 절감에도 연결된다.
입력·실행·정리에서 놓치기 쉬운 지점
입력에는 전제조건, 입력값, 기대값, 환경 변수가 담긴 케이스 정의가 필요하다. 테스트 데이터 시드에는 ID 네임스페이스와 시간 고정 조건을 함께 둔다.
실행 단계는 드라이버 기동→SUT 호출→스텁 응답→어서션 평가로 이어진다. DB 테스트는 케이스 단위 트랜잭션 롤백과 임시 스키마 전략 중 하나를 선택해 트랜잭션과 락을 관리한다.
출력은 결과 리포트, 성공/실패/스킵 요약 메트릭, 업로드된 아티팩트로 구성된다. 타임아웃과 재시도, 지연 실패(fail-fast vs complete-on-error) 정책도 명시해야 한다. 실패 샌드박스 보존은 로컬에서만 선택적으로 허용하고, CI에서는 아티팩트로 대체한 뒤 리소스를 즉시 삭제한다.
현실성과 유지비 사이의 선택
실서비스 호출은 현실성이 높아지는 대신 속도는 낮아지고 플래키는 늘어난다. 스텁은 반대의 특성을 가지며, 계약 테스트가 두 방식의 균형을 잡는 역할을 한다.
골든 데이터세트는 안정성을 높이지만 커버리지는 낮아질 수 있다. 데이터 빌더 기반 생성은 커버리지를 높이는 대신 유지비가 증가한다. 테스트 데이터에는 PII 무마스킹 원칙을 적용하고, 비밀정보는 시크릿 매니저로 주입하며 로컬 파일에 두지 않는다.
컨테이너, 볼륨, 임시 스키마, 임시 파일, 키-값 네임스페이스는 테스트 후 자동으로 정리해야 한다. 실패 케이스를 재현할 때만 로컬에서 제한적으로 보존하며, CI에서는 보존하지 않는 원칙을 둔다.
수동 실행과 임시 스크립트의 차이
| 항목 | 수동 실행 | 임시 스크립팅 | 테스트하네스 |
|---|---|---|---|
| 성능(스루풋/시간) | 낮음/지연 큼 | 중간 | 높음/병렬 최적화 |
| 확장성(케이스 증가 비용) | 선형↑ | 준선형↑ | 아키텍처 재사용으로 완만 |
| 일관성(재현성) | 낮음 | 중간 | 높음(결정성 보장) |
| 안정성(플래키율) | 높음 | 중간 | 낮음(격리/제어) |
| 운영 편의(CI/CD) | 낮음 | 제한적 | 높음(표준화·아티팩트) |
드라이버와 스텁을 한 파일에서 연결하기
환경/전제조건: Python 3.11+, 외부 라이브러리 불필요. 단일 파일 실행 가능.
# file: harness_example.py
# --- SUT (외부 의존 호출 포함) ---
def get_tier(user_id: str) -> str:
# 실제 환경에서는 외부 API 호출됨
raise RuntimeError("External call not allowed in tests")
def price_for_user(user_id: str, base: int) -> int:
tier = get_tier(user_id)
rate = {"GOLD": 0.2, "SILVER": 0.1}.get(tier, 0.0)
return int(round(base * (1 - rate)))
# --- Stub (외부 의존 대체) ---
class StubLoyalty:
def __init__(self, mapping: dict[str, str]):
self.mapping = dict(mapping)
def get_tier(self, user_id: str) -> str:
return self.mapping.get(user_id, "BRONZE")
def clear(self):
self.mapping.clear()
# --- Driver (케이스 실행, 어서션, 정리) ---
def run_case(user_id: str, base: int, expected: int, stub: StubLoyalty) -> None:
original = globals()["get_tier"]
try:
globals()["get_tier"] = stub.get_tier # monkey patch
result = price_for_user(user_id, base)
assert result == expected, f"expected={expected}, got={result}"
finally:
globals()["get_tier"] = original # 복원(격리)
# 테스트 데이터 정리(테스트 후 반드시 삭제)
stub.clear()
if __name__ == "__main__":
stub = StubLoyalty({"u1": "GOLD", "u2": "SILVER"})
# 케이스 1
run_case("u1", base=100, expected=80, stub=stub)
# 케이스 2
stub = StubLoyalty({"u2": "SILVER"})
run_case("u2", base=100, expected=90, stub=stub)
print("OK - all cases passed")
이 예시에서 드라이버는 케이스 데이터→실행→검증→정리를 맡는다. 스텁은 외부 호출을 차단해 결정성을 보장하며, 테스트 데이터는 케이스 종료 시 clear 호출로 삭제된다.
테스트하네스는 드라이버, 스텁, 테스트 데이터, 오케스트레이션, 검증을 하나의 실행 기반으로 묶는다. 계약 테스트와 병행하고 데이터 빌더, 컨테이너 격리, CI 병렬화를 결합하면 결정성·격리성·정리 자동화를 바탕으로 회귀 테스트를 운영할 수 있다.