본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 외부 API 테스트를 위한 Mocking과 Patching의 3가지 차이점과 해결 방법

by Papa Martino V 2026. 3. 29.
728x90

Mocking과 Patching
Mocking과 Patching

 

네트워크 의존성을 제거하고 독립적인 테스트 환경을 구축하는 파이썬 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(테스트 주도 개발)의 핵심은 외부 시스템에 휘둘리지 않고 내가 짠 로직의 순수성을 보장하는 데 있습니다.


참고 및 출처:

  • Python Documentation: "unittest.mock — mock object library"
  • Real Python: "Understanding the Python Mock Object Library"
  • Pytest Documentation: "Monkeypatching and mocking modules and environments"
  • "Test-Driven Development with Python" by Harry Percival
728x90