메뉴 닫기

파이썬 requests 테스트 모킹 가이드 responses와 requests-mock로 HTTP 스텁 정확한 매칭 시나리오

파이썬 requests 테스트 모킹 가이드 responses와 requests-mock로 HTTP 스텁 정확한 매칭 시나리오

🧪 실무 테스트에서 외부 API 의존성을 제거하고 정확한 시나리오를 재현하는 두 라이브러리 비교와 베스트 프랙티스

외부 API를 호출하는 코드를 안전하게 바꾸거나, 네트워크가 없어도 빠르게 테스트를 돌리고 싶을 때 가장 먼저 떠오르는 도구가 바로 HTTP 모킹입니다.
responses와 requests-mock은 requests 기반 프로젝트에서 널리 쓰이는 대표적인 선택지로, 간단한 스텁부터 정교한 매칭, 다단계 시나리오까지 폭넓게 다룰 수 있습니다.
이 글은 두 도구의 차이와 강점을 이해하고, 팀 코드베이스에 자연스럽게 적용하도록 돕는 길잡이입니다.
현업에서 자주 겪는 성공·실패 응답 전환, 상태코드와 헤더·쿼리·바디의 정확한 매칭, 세션 기반 호출 흐름까지 자연스럽게 녹여 설명합니다.

테스트를 작성할 때 가장 중요한 건 재현 가능성과 신뢰도입니다.
임의의 더미 응답만 던져주는 수준을 넘어, 실제 API 계약을 반영한 스텁과 검증 가능한 매칭 규칙을 갖추면 회귀 버그를 초기에 차단할 수 있습니다.
responses는 선언적으로 엔드포인트를 정의해 빠르게 시작하기 좋고, requests-mock은 어댑터와 세션을 활용해 미세 조정과 기록·검증에 유리합니다.
각 도구가 제공하는 기능을 목적에 맞게 조합하면, flaky한 네트워크 의존을 제거하면서도 코드 리팩터링 속도를 끌어올릴 수 있습니다.



🧪 responses로 HTTP 스텁 기본 사용법

responses는 requests 호출을 가로채 원하는 응답을 돌려주는 방식으로 동작합니다.
가장 큰 장점은 짧은 코드로 엔드포인트를 신속하게 스텁할 수 있고, 메서드·URL·쿼리·헤더·바디까지 정확히 매칭해 재현 가능한 테스트를 만들 수 있다는 점입니다.
또한 호출 검증과 호출 순서 제어, 예외 발생 시나리오(타임아웃 등)까지 다뤄 동일한 테스트가 언제 돌려도 같은 결과를 보장하도록 돕습니다.

핵심 개념은 세 가지입니다.
첫째, responses.add()로 스텁을 정의합니다.
둘째, @responses.activate 혹은 컨텍스트 매니저로 패치를 시작·종료합니다.
셋째, matchers를 사용해 쿼리·헤더·바디를 엄격하게 검증합니다.
아래 예제는 성공/실패 응답을 단계별로 준비하고, 요청이 기대와 다르면 테스트가 즉시 실패하도록 설계한 기본 패턴입니다.

CODE BLOCK
import json
import requests
import responses
from responses import matchers

API = "https://api.example.com/v1/users"

def fetch_user(user_id, token):
    r = requests.get(
        f"{API}/{user_id}",
        headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
        params={"verbose": "1"},
        timeout=5,
    )
    r.raise_for_status()
    return r.json()

@responses.activate
def test_fetch_user_success_then_unauthorized():
    # 1) 정확한 매칭: 메서드, URL, 쿼리, 헤더, 바디
    responses.add(
        method=responses.GET,
        url=f"{API}/42?verbose=1",
        json={"id": 42, "name": "Ada"},
        status=200,
        headers={"X-Trace-Id": "T-001"},
        match=[
            matchers.header_matcher({"Authorization": "Bearer token-OK", "Accept": "application/json"}),
        ],
    )

    # 2) 실패 시나리오(토큰 만료)
    responses.add(
        method=responses.GET,
        url=f"{API}/42?verbose=1",
        json={"error": "unauthorized"},
        status=401,
        match=[matchers.header_matcher({"Authorization": "Bearer token-EXPIRED"})],
    )

    # 호출 1: 성공
    assert fetch_user(42, "token-OK") == {"id": 42, "name": "Ada"}

    # 호출 2: 401 발생 확인
    try:
        fetch_user(42, "token-EXPIRED")
        assert False, "should raise HTTPError"
    except requests.HTTPError as e:
        assert e.response.status_code == 401

    # 정의된 스텁이 모두 사용되었는지 검증(옵션)
    responses.assert_all_requests_are_fired()

위 예제는 동일한 URL이라도 헤더 매칭을 다르게 두어 토큰 상태에 따라 서로 다른 응답을 순차 제공하는 구성입니다.
실제 서비스에서 흔한 성공 → 토큰 만료 흐름을 간단하게 재현할 수 있으며, responses.assert_all_requests_are_fired()를 통해 미사용 스텁이나 누락된 호출을 빠르게 탐지합니다.
또한 matchers.query_param_matcher로 쿼리를 dict 형태로 검증하거나, json_params_matcher로 POST 바디를 엄격히 검사할 수 있습니다.

핵심 기능 설명
정확한 매칭 메서드, 정규식 URL, 쿼리, 헤더, JSON 바디를 matchers로 세밀하게 검증합니다.
순차 응답 동일 엔드포인트에 여러 응답을 등록해 성공→실패 등 시나리오를 재현합니다.
호출 검증 미사용 스텁 또는 누락 호출을 assert_all_requests_are_fired로 빠르게 파악합니다.
  • 🧩@responses.activate 또는 컨텍스트 매니저로 패치 범위를 최소화합니다.
  • 🎯쿼리·헤더·바디는 matchers로 엄격 매칭해 계약 위반을 조기에 발견합니다.
  • 🔁동일 엔드포인트에 여러 응답을 등록해 단계적 시나리오를 재현합니다.
  • 🧪미사용 스텁·누락 호출은 assert_all_requests_are_fired()로 점검합니다.

💡 TIP: 복잡한 URL은 정규식(re.compile)로 정의하고, 공통 헤더는 테스트 픽스처에서 합성해 중복을 줄이면 유지보수가 쉬워집니다.

⚠️ 주의: 스텁이 실제 API 스키마와 어긋나면 테스트가 거짓 안정성을 줄 수 있습니다.
계약(상태코드, 헤더, 스키마) 변경 시 스텁을 반드시 함께 업데이트하세요.

🧭 requests-mock로 세밀한 매칭과 세션 지원

requests-mock은 requests의 어댑터 구조를 직접 활용해 네트워크 계층을 가짜로 대체합니다.
즉, Session 단위로 세밀하게 스텁을 주입하거나, 특정 테스트에서만 mock 환경을 적용할 수 있습니다.
responses가 전역 패치 기반이라면, requests-mock은 어댑터 기반이라는 점이 가장 큰 차이입니다.
이 덕분에 테스트 간 격리와 병렬 실행이 안정적으로 이루어집니다.

가장 흔한 사용 패턴은 pytest의 fixture로 등록하는 방식입니다.
테스트마다 다른 URL이나 응답을 세팅할 수 있고, 실제 requests.Session 인스턴스와 자연스럽게 연결됩니다.
또한 request_history 속성을 통해 어떤 요청이 일어났는지, 어떤 헤더나 파라미터가 전달됐는지를 검증할 수도 있습니다.

CODE BLOCK
import requests
import requests_mock
import pytest

API = "https://api.example.com/v1/items"

@pytest.fixture
def client():
    return requests.Session()

def get_item(client, item_id):
    r = client.get(f"{API}/{item_id}")
    r.raise_for_status()
    return r.json()

def test_get_item_with_mock(client):
    with requests_mock.Mocker() as m:
        # URL별 스텁 등록
        m.get(f"{API}/123", json={"id": 123, "name": "Book"}, status_code=200)
        m.get(f"{API}/404", status_code=404, text="Not Found")

        # 요청 1: 정상 응답
        result = get_item(client, 123)
        assert result["name"] == "Book"

        # 요청 2: 404 발생
        with pytest.raises(requests.exceptions.HTTPError):
            get_item(client, 404)

        # 요청 내역 확인
        history = m.request_history
        assert len(history) == 2
        assert history[0].url.endswith("/123")

이처럼 requests-mock은 실제 요청을 어댑터 계층에서 가로채기 때문에, Session별 격리나 다중 클라이언트 환경에서도 충돌이 없습니다.
또한 특정 스텁만 임시로 등록했다가 컨텍스트를 빠져나오면 자동으로 해제되어 테스트 환경이 깨끗하게 유지됩니다.

💬 requests-mock은 responses보다 더 세밀한 세션 단위 제어와 기록 기능을 제공합니다. 대규모 테스트 환경에서 병렬 실행 시 안정성이 높습니다.

  • 🔧테스트마다 새로운 Session을 생성하면 스텁 간 간섭을 방지할 수 있습니다.
  • 🧩m.request_history를 활용해 호출 횟수·URL·파라미터를 검증합니다.
  • ⚙️정규식 URL이나 additional_matcher로 복잡한 매칭을 구현할 수 있습니다.
  • 🧪responses보다 빠른 초기화와 teardown으로 대규모 테스트에 적합합니다.

💎 핵심 포인트:
responses가 선언형 DSL에 가깝다면, requests-mock은 실시간 세션 제어형 도구입니다. 두 가지를 함께 쓰면 속도와 제어력을 모두 확보할 수 있습니다.



🧱 정확한 매칭 전략 상태코드 헤더 쿼리 바디

HTTP 요청 테스트의 핵심은 얼마나 정확히 매칭할 수 있느냐입니다.
단순히 URL만 맞춘다고 해서 신뢰할 수 있는 테스트가 되는 것은 아닙니다.
요청 메서드, 쿼리 파라미터, 헤더, 바디(특히 JSON)까지 모두 일치해야 실제 운영 API 계약을 재현할 수 있습니다.
responses와 requests-mock은 각각 matchers 혹은 additional_matcher를 제공하여 이 과정을 세밀하게 제어할 수 있게 해줍니다.

responses에서는 기본적으로 responses.add()match=[…매처…]를 전달해 세부 항목을 검증합니다.
반면 requests-mock은 등록 시점에 additional_matcher라는 콜백을 넣어 커스텀 매칭 로직을 직접 구현할 수 있습니다.
예를 들어, Authorization 헤더나 JSON 필드 일부만 일치해야 통과하도록 제어할 수 있습니다.

CODE BLOCK
import json
import requests
import responses
from responses import matchers

@responses.activate
def test_exact_matchers():
    responses.add(
        responses.POST,
        "https://api.service.com/login",
        json={"status": "ok"},
        status=200,
        match=[
            matchers.query_param_matcher({"lang": "ko"}),
            matchers.header_matcher({"Content-Type": "application/json"}),
            matchers.json_params_matcher({"username": "admin", "password": "1234"}),
        ],
    )

    res = requests.post(
        "https://api.service.com/login?lang=ko",
        headers={"Content-Type": "application/json"},
        json={"username": "admin", "password": "1234"},
    )
    assert res.json()["status"] == "ok"

이처럼 매칭 규칙을 세밀히 지정하면 예상치 못한 파라미터 누락이나 잘못된 헤더 값이 있을 경우 테스트가 즉시 실패합니다.
이는 API 계약서와 실제 구현 간의 차이를 조기에 찾아내는 매우 효과적인 방법입니다.
requests-mock에서도 다음과 같이 additional_matcher로 동일한 효과를 낼 수 있습니다.

CODE BLOCK
import requests_mock

def check_auth(request):
    return request.headers.get("Authorization") == "Bearer test-123"

with requests_mock.Mocker() as m:
    m.get("https://secure.api/data", json={"ok": True}, additional_matcher=check_auth)
    r = requests.get("https://secure.api/data", headers={"Authorization": "Bearer test-123"})
    assert r.status_code == 200

이 방식은 동적으로 조건을 검사할 수 있어, 토큰이나 타임스탬프, 서명값 같은 민감한 인증 로직 테스트에 유용합니다.
또한 responses의 matcher는 JSON만 아니라 multipart/form-data 업로드나 raw 데이터도 정확히 매칭할 수 있습니다.
이러한 세밀한 제어가 누적될수록 테스트는 더 신뢰성 있는 회귀 검증 도구로 진화합니다.

💎 핵심 포인트:
테스트는 단순히 “응답이 왔다”가 아니라 “요청이 정확히 예상한 형태로 보냈다”를 증명해야 합니다. responses와 requests-mock의 매칭 기능은 이를 자동으로 보장하는 핵심입니다.

  • 🧩헤더·쿼리·바디 매칭을 활용해 계약 위반을 조기 감지합니다.
  • 🔍추가 검증이 필요한 경우 additional_matcher로 커스텀 로직을 작성합니다.
  • 🧪JSON 외에도 multipart/form-data, binary 요청도 동일하게 매칭 가능합니다.
  • ⚙️매칭 실패 시 테스트는 실패해야 하며, 이를 통해 잘못된 요청 흐름을 조기에 차단합니다.

🎯 시나리오 기반 모킹 단계적 응답과 실패 케이스

실제 서비스에서는 동일한 API를 여러 번 호출하더라도 매번 다른 결과가 돌아오는 경우가 많습니다.
예를 들어, 첫 번째 호출은 성공하고 두 번째는 인증 만료, 세 번째는 서버 에러처럼 상태 전이(State Transition)가 있는 시나리오입니다.
이럴 때 단일 응답만 등록하면 현실적인 테스트가 어렵습니다.
responses와 requests-mock은 모두 동일 URL에 여러 응답을 등록해 순차적으로 반환하도록 지원합니다.

responses에서는 동일한 URL을 add()로 여러 번 추가하면, 요청 순서에 따라 하나씩 반환됩니다.
이를 활용하면 성공 → 실패 → 재시도 성공과 같은 실제 서비스 흐름을 그대로 시뮬레이션할 수 있습니다.
아래 예제는 사용자 인증 API가 토큰 만료 후 재발급을 거쳐 정상 응답으로 돌아오는 과정을 테스트합니다.

CODE BLOCK
import requests
import responses

API = "https://api.example.com/login"

def login():
    r = requests.post(API, json={"user": "test", "pw": "1234"})
    r.raise_for_status()
    return r.json()

@responses.activate
def test_login_retry_scenario():
    # 1️⃣ 첫 호출: 401 Unauthorized
    responses.add(responses.POST, API, json={"error": "unauthorized"}, status=401)
    # 2️⃣ 두 번째 호출: 토큰 갱신 성공
    responses.add(responses.POST, API, json={"token": "abc123"}, status=200)

    try:
        login()
    except requests.HTTPError:
        # 실패 후 재시도
        result = login()
        assert result["token"] == "abc123"

requests-mock에서도 동일한 기능을 m.get()이나 m.post()에 응답 리스트를 전달해 구현할 수 있습니다.
이 방식은 상태 기반 시스템(예: 로그인 흐름, 주문 처리, 결제 시나리오)을 단위 테스트 수준에서 완벽히 재현하게 해 줍니다.

CODE BLOCK
import requests_mock

with requests_mock.Mocker() as m:
    m.get(
        "https://api.example.com/order",
        [
            {"json": {"status": "processing"}, "status_code": 200},
            {"json": {"status": "done"}, "status_code": 200},
        ],
    )

    import requests
    r1 = requests.get("https://api.example.com/order")
    r2 = requests.get("https://api.example.com/order")

    assert r1.json()["status"] == "processing"
    assert r2.json()["status"] == "done"

이처럼 시나리오 기반 모킹은 복잡한 비즈니스 로직의 안정성을 높이는 핵심 요소입니다.
단위 테스트에서 상태 변화를 시뮬레이션하면, 실제 네트워크 호출을 하지 않고도 서비스 전반의 로직을 재현할 수 있습니다.
또한 실패 케이스(타임아웃, 500 에러 등)를 미리 정의하면, 예외 처리 로직이 올바르게 동작하는지도 쉽게 검증할 수 있습니다.

💡 TIP: 실패 케이스를 항상 성공 케이스와 함께 정의하세요.
API가 정상 작동할 때뿐 아니라 장애 상황에서도 시스템이 복원력을 유지하는지 검증하는 것이 중요합니다.

  • 🔁동일 URL에 여러 응답을 등록해 순차 시나리오를 재현합니다.
  • ⚠️401, 500 등 에러 시나리오를 반드시 포함해 예외 처리 테스트를 강화합니다.
  • 🧪상태 전이 테스트를 통해 로직이 연속적인 호출에도 안정적으로 동작하는지 확인합니다.
  • 📈responses와 requests-mock 모두 순차 응답 기능을 지원하므로 상황에 맞게 선택하세요.



🧰 테스트 구조화 픽스처 패턴과 유지보수 팁

responses나 requests-mock으로 테스트를 작성하다 보면, 스텁 코드가 여러 테스트 파일에 중복되는 경우가 많습니다.
이럴 땐 pytest fixture를 활용해 재사용 가능한 모킹 환경을 구성하는 것이 좋습니다.
공통 스텁을 중앙 관리하면 유지보수성과 일관성이 크게 향상됩니다.
특히 API 버전이 바뀔 때 한 곳만 수정해도 전체 테스트가 자동으로 업데이트됩니다.

responses는 @responses.activate를 사용하는 대신 fixture에서 컨텍스트 매니저를 열어 테스트 범위 전체를 패치할 수 있고, requests-mock도 mocker를 fixture로 주입해 전역에서 재사용할 수 있습니다.
이 방식은 테스트 간의 상태 누락, 중복 호출, URL 오타를 줄이는 데 큰 도움이 됩니다.

CODE BLOCK
import pytest
import responses
import requests

@pytest.fixture
def mock_api():
    with responses.RequestsMock() as rsps:
        rsps.add(responses.GET, "https://api.test.io/v1/ping", json={"pong": True}, status=200)
        yield rsps

def ping():
    return requests.get("https://api.test.io/v1/ping").json()

def test_ping(mock_api):
    result = ping()
    assert result["pong"] is True
    assert mock_api.calls[0].response.status_code == 200

이처럼 fixture를 통해 모킹 환경을 구성하면, 테스트 함수가 간결해지고 각 케이스에 집중할 수 있습니다.
여기에 pytest parametrization을 결합하면 다양한 요청/응답 조합을 손쉽게 반복 검증할 수 있습니다.
또한 requests-mock에서는 requests_mock.Mocker()를 fixture로 등록해 동일한 패턴을 사용할 수 있습니다.

💬 테스트의 유지보수성은 ‘중복 제거’와 ‘명확한 책임 분리’에서 시작됩니다. responses와 requests-mock의 fixture화를 통해 모킹 코드도 하나의 인프라처럼 관리할 수 있습니다.

  • 🧩공통 API 스텁은 pytest fixture로 관리해 재사용성을 높입니다.
  • 📦responses.RequestsMock() 또는 requests_mock.Mocker()를 fixture에서 yield로 관리합니다.
  • 🧠테스트마다 동일한 모킹 구조를 유지해 신뢰도 있는 회귀 테스트를 보장합니다.
  • ⚙️fixture 내부에서 등록된 응답은 테스트 종료 시 자동 해제되어 환경이 깨끗하게 유지됩니다.

💎 핵심 포인트:
모킹은 단순히 외부 API를 가짜로 만드는 행위가 아니라, 테스트 인프라의 일부입니다. 구조화된 fixture 패턴은 장기적인 품질과 속도를 모두 확보할 수 있는 전략입니다.

자주 묻는 질문 (FAQ)

responses와 requests-mock 중 어떤 걸 선택해야 하나요?
responses는 빠르게 시작하기 좋은 선언형 라이브러리이고, requests-mock은 세션 기반의 정교한 제어가 가능합니다.
소규모 테스트엔 responses, 대규모 병렬 환경엔 requests-mock이 적합합니다.
responses.add를 여러 번 호출하면 어떤 순서로 동작하나요?
동일한 URL에 대해 add()를 여러 번 호출하면, 등록 순서대로 응답이 반환됩니다.
즉 첫 번째 요청엔 첫 번째 응답, 두 번째 요청엔 두 번째 응답이 전달됩니다.
매칭 실패 시 테스트는 어떻게 처리되나요?
매칭이 실패하면 responses는 “Connection refused” 예외를 발생시키고, requests-mock은 등록되지 않은 URL 요청으로 간주해 에러를 반환합니다.
둘 다 테스트 실패로 간주됩니다.
responses.matchers를 사용하면 JSON 내부 순서도 검증되나요?
아니요. JSON 키의 순서는 기본적으로 무시됩니다.
다만 값 비교는 엄격하게 일치해야 하므로, 필드 누락이나 오타는 즉시 테스트 실패로 이어집니다.
requests-mock의 additional_matcher는 어떤 상황에 유용한가요?
복잡한 인증 헤더, 서명 값, 타임스탬프처럼 단순 비교가 어려운 상황에서 유용합니다.
콜백 내부에서 직접 요청 객체를 검사할 수 있기 때문입니다.
실제 네트워크를 일부만 모킹할 수 있나요?
가능합니다.
responses에서는 allow_external=True로, requests-mock에서는 real_http=True로 설정하면 등록되지 않은 요청은 실제 네트워크로 전달됩니다.
비동기 코드(asyncio)에서도 responses를 쓸 수 있나요?
responses는 requests 전용이기 때문에 비동기 aiohttp 환경에서는 사용할 수 없습니다.
aiohttp를 테스트할 때는 aresponses 또는 respx 같은 별도 라이브러리를 사용해야 합니다.
responses와 requests-mock을 함께 쓸 수 있나요?
원칙적으로 가능하지만 권장되지는 않습니다.
테스트 스코프가 충돌할 수 있으므로, 하나의 프레임워크에서 일관성 있게 관리하는 편이 더 안전합니다.

🚀 responses와 requests-mock을 활용한 신뢰성 높은 API 테스트 전략

responses와 requests-mock은 단순히 요청을 가짜로 만드는 도구를 넘어, API 품질을 보장하는 강력한 테스트 인프라입니다.
responses는 직관적이고 빠른 스텁 작성에 강점을 가지며, requests-mock은 세션 단위의 정교한 제어와 기록 기능을 제공합니다.
두 도구를 목적에 맞게 병행하거나, 프로젝트 규모에 맞춰 선택한다면 테스트의 신뢰도와 유지보수성이 크게 향상됩니다.

특히 상태코드, 헤더, 바디, 쿼리 매칭을 엄격하게 구성하면 실제 서비스와 거의 동일한 환경에서 코드를 검증할 수 있습니다.
이러한 세밀한 테스트는 예기치 못한 회귀 버그를 조기에 잡고, 외부 네트워크 의존성을 제거해 빌드 속도도 높여줍니다.
또한 시나리오 기반 응답, 픽스처 패턴, 커스텀 매칭 로직을 활용하면 복잡한 서비스 로직도 안전하게 검증할 수 있습니다.

즉, responses와 requests-mock을 잘 활용하면 네트워크 테스트의 불안정성을 해결하고, 지속 가능한 테스트 아키텍처를 구축할 수 있습니다.
테스트 코드가 단순한 보조 수단이 아니라, 품질 보증의 핵심으로 발전하는 과정을 직접 경험할 수 있을 것입니다.


🏷️ 관련 태그 : 파이썬테스트, requests, responses, requests-mock, HTTP모킹, API테스트, pytest, 개발자동화, 테스트코드, 소프트웨어품질