Python 테스트 더블: 외부 경계만 대체하고 결과 검증하기
순수 함수, 주입한 가짜 객체, 통합 테스트를 나눠 mock 과용을 줄이는 예제입니다.
테스트에서 모든 함수를 mock으로 바꾸면 구현이 지정한 순서로 호출되는지는 확인해도 실제 결과가 맞는지는 놓칠 수 있습니다. 먼저 순수한 계산을 실제로 실행하고, 네트워크처럼 통제하기 어려운 경계만 대체하는 방식으로 시작해 보겠습니다.
계산은 실제 값으로 검증하기
아래 예제는 금액 단위를 정수로 다루는 단순 배송비 계산입니다. 실제 요금 정책이 아니라 테스트 설계용 가상 규칙입니다.
def total_with_shipping(subtotal):
if subtotal < 0:
raise ValueError("subtotal must be nonnegative")
return subtotal + (0 if subtotal >= 30000 else 3000)
assert total_with_shipping(29999) == 32999
assert total_with_shipping(30000) == 30000
경계값 바로 아래와 경계값 자체를 검사합니다. 이 함수의 내부 덧셈이나 조건문을 mock으로 바꿀 이유는 없습니다. 구현을 다른 방식으로 바꾸어도 결과 규칙이 같으면 테스트는 통과해야 합니다.
외부 경계를 주입하기
알림 발송 함수는 호출자가 넘겨주도록 만들 수 있습니다. 테스트에서는 실제 이메일을 보내지 않고 전달될 내용을 수집합니다.
def notify_total(subtotal, send):
total = total_with_shipping(subtotal)
send({"total": total})
return total
sent = []
assert notify_total(10000, sent.append) == 13000
assert sent == [{"total": 13000}]
이 테스트는 계산과 전달 데이터의 연결을 검증합니다. 실제 이메일 공급자가 메시지를 받는지, 인증이 유효한지까지 검증하는 것은 아닙니다. 경계의 계약은 별도의 통합 테스트로 확인합니다.
mock이 필요한 경우의 주의점
호출 횟수가 요구사항일 때만 횟수 검증을 사용합니다. 예를 들어 중복 청구 방지는 중요하지만 내부 헬퍼가 한 번 호출되는지는 리팩터링에 따라 바뀔 수 있습니다. Python unittest.mock을 사용할 때 spec이나 autospec은 존재하지 않는 속성이나 잘못된 호출을 발견하는 데 도움이 됩니다.
패치는 원래 정의된 모듈이 아니라 코드가 그 이름을 참조하는 위치를 기준으로 이해해야 합니다. 모듈 경로를 잘못 패치하면 테스트가 실제 외부 호출을 실행할 수 있습니다.
실패한 테스트 읽는 순서
같은 테스트가 단독으로도 실패하는지, 시간·랜덤·환경변수·DB 상태에 의존하는지 확인합니다. CI에서만 실패하면 버전과 시간대, 병렬 실행에서 공유한 자원을 비교합니다. 실패를 없애기 위해 검증문을 약하게 바꾸기 전에 요구사항과 구현 중 무엇이 잘못됐는지 판단합니다.
레거시 개선에서는 기존 동작을 기록하는 특성 테스트부터 추가할 수 있습니다. 다만 기존 동작이 버그일 수 있으므로 ‘현재 이렇게 동작함’과 ‘계속 보장할 계약’을 구분해 기록합니다.