
네트워크 의존성을 제거하고 독립적인 테스트 환경을 구축하는 파이썬 unittest.mock 마스터 가이드입니다.
1. 서론: 왜 외부 API 호출을 직접 테스트하면 안 되는가?
소프트웨어 개발 실무에서 외부 API(결제 모듈, 날씨 정보, 소셜 로그인 등)와의 연동은 필수적입니다. 그러나 실제 유닛 테스트 단계에서 라이브 API를 호출하는 것은 다음과 같은 심각한 문제점을 야기합니다.
- 비결정론적 결과: 외부 서버 상태에 따라 테스트 성공 여부가 달라집니다.
- 속도 저하: 네트워크 레이턴시로 인해 전체 CI/CD 파이프라인이 느려집니다.
- 비용 발생: 호출당 과금이 발생하는 API의 경우 테스트 비용이 폭증합니다.
- 데이터 오염: 실제 운영 DB나 외부 서비스에 테스트 데이터가 쌓이게 됩니다.
이러한 문제를 해결하기 위해 파이썬에서는 Mocking(가짜 객체 생성)과 Patching(기존 객체를 가짜로 교체) 기법을 사용합니다. 본 글에서는 이 두 기법의 본질적인 차이를 분석하고, 실무에서 즉시 적용 가능한 7가지 고급 예제를 제안합니다.
2. Mocking vs Patching: 개념적 차이와 활용 상황 비교
테스트 대역(Test Double)을 만드는 두 핵심 기술의 차이점을 표로 정리하였습니다.
| 비교 항목 | Mocking (unittest.mock.Mock) | Patching (unittest.mock.patch) |
|---|---|---|
| 핵심 정의 | 실제 객체처럼 행동하는 가짜 객체를 직접 생성 | 특정 경로의 객체를 런타임에 Mock으로 일시적 교체 |
| 사용 위치 | 의존성 주입(DI)이 가능한 구조에서 인자로 전달 | 함수 내부에서 고정된 라이브러리(requests 등)를 가로챌 때 |
| 수명 주기 | 변수에 할당되어 있는 동안 유지 | Context Manager나 Decorator 범위 내에서만 유지 |
| 구현 난이도 | 낮음 (단순 객체 생성) | 중간 (정확한 '임포트 경로' 지정이 중요) |
| 주요 해결 방법 | m = Mock(return_value=200) |
@patch('module.target_class') |
3. 실무 최적화 Mocking & Patching 해결 Example (7선)
파이썬 테스트 프레임워크인 pytest와 내장 unittest.mock을 혼합하여 사용하는 실무 예제입니다.
Example 1: requests.get 호출 가로채기 (Basic Patch)
가장 흔한 사례로, 특정 URL 호출 시 미리 정의된 응답을 반환하게 합니다.
from unittest.mock import patch
import requests
def get_api_status(url):
response = requests.get(url)
return response.status_code
@patch('requests.get')
def test_get_api_status(mock_get):
# Mock 응답 설정
mock_get.return_value.status_code = 200
result = get_api_status("http://fake-api.com")
assert result == 200
mock_get.assert_called_once_with("http://fake-api.com")
Example 2: Side Effect를 이용한 예외 상황 테스트
네트워크 타임아웃이나 서버 에러 발생 시의 로직을 검증합니다.
from unittest.mock import Mock
import requests
def call_unreliable_api():
try:
response = requests.get("https://api.com")
return response.json()
except requests.exceptions.Timeout:
return {"error": "timeout"}
def test_api_timeout():
mock_requests = Mock()
# 예외 발생 시뮬레이션
mock_requests.get.side_effect = requests.exceptions.Timeout()
# 로직 검증
# ... (생략)
Example 3: Context Manager를 활용한 국소 범위 Patching
데코레이터 대신 with 문을 사용하여 특정 코드 블록만 가짜로 바꿉니다.
def test_partial_mock():
with patch('app.services.ExternalAPI.fetch') as mock_fetch:
mock_fetch.return_value = {"data": "ok"}
# 이 블록 안에서만 mock_fetch가 작동함
# ...
Example 4: 다중 리턴값 설정 (Iterable Side Effect)
동일한 함수를 여러 번 호출할 때 매번 다른 값을 반환하게 합니다. (예: 재시도 로직 테스트)
@patch('time.sleep') # 테스트 속도를 위해 sleep도 mocking
@patch('requests.get')
def test_retry_logic(mock_get, mock_sleep):
# 첫 두 번은 실패, 세 번째는 성공
mock_get.side_effect = [Exception("Fail"), Exception("Fail"), Mock(status_code=200)]
# 재시도 로직 수행 함수 호출...
Example 5: API 응답 JSON 객체 정교하게 묘사하기
단순 값이 아닌 .json() 메서드까지 포함한 실제 Response 객체처럼 만듭니다.
def test_complex_json_response():
with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 123, "status": "active"}
mock_get.return_value = mock_response
# ...
Example 6: 'Where to patch' 문제 해결 (Import 경로)
클래스가 정의된 곳이 아니라, 사용되는 곳(Target)을 패치해야 하는 파이썬의 특성을 반영합니다.
# app/logic.py 에서 'from app.client import APIClient'를 한다면
# 패치 경로는 'app.client.APIClient'가 아니라 'app.logic.APIClient'여야 함
@patch('app.logic.APIClient')
def test_logic(mock_client_class):
instance = mock_client_class.return_value
instance.call.return_value = True
# ...
Example 7: MagicMock을 이용한 매직 메서드 시뮬레이션
객체의 __len__, __getitem__ 등 특수 동작이 필요할 때 사용합니다.
from unittest.mock import MagicMock
def test_iterable_mock():
mock_api = MagicMock()
mock_api.__len__.return_value = 5
assert len(mock_api) == 5
4. 결론: 깨지지 않는 테스트를 위한 최적의 전략
Mocking과 Patching은 강력하지만, 실제 구현 코드와 너무 밀접하게 결합되면 코드가 조금만 바뀌어도 테스트가 깨지는 'Fragile Test'의 원인이 됩니다. 이를 방지하기 위한 해결 전략은 다음과 같습니다.
- 의존성 주입 선호:
patch보다는 생성자나 인자를 통해Mock객체를 주입하는 설계를 지향하십시오. - 최소한의 범위만 패치: 전역적인 패치보다는 필요한 함수 단위로 범위를 제한하십시오.
- 검증(Assertion) 필수: 단순히 값을 돌려받는 것에 그치지 않고,
assert_called_with등을 통해 올바른 인자로 호출되었는지 반드시 확인하십시오.
결국 TDD(테스트 주도 개발)의 핵심은 외부 시스템에 휘둘리지 않고 내가 짠 로직의 순수성을 보장하는 데 있습니다.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 어댑터 패턴으로 레거시 코드를 통합하는 7가지 방법과 구조적 차이 해결 가이드 (0) | 2026.03.29 |
|---|---|
| [PYTHON] unittest와 pytest의 5가지 차이점 분석 및 pytest가 대세가 된 해결 방법 (0) | 2026.03.29 |
| [PYTHON] @dataclass와 NamedTuple, 일반 클래스의 용도 차이 해결 방법과 7가지 실무 사례 (0) | 2026.03.29 |
| [PYTHON] 인터페이스 규약 강제를 위한 NotImplementedError 활용 방법 3가지와 구조적 차이점 (0) | 2026.03.29 |
| [PYTHON] 런타임 함수 호출 횟수를 줄이는 인라이닝(Inlining) 기법과 2가지 핵심 한계 해결 방법 (0) | 2026.03.28 |