StackPractices
intermediate By Mathias Paulenko

Mock External APIs with responses Library

How to mock HTTP API calls in Python tests using the responses library, including status codes, headers, JSON bodies, and error simulation.

Topics: testing

Overview

The responses library intercepts HTTP requests made by the requests library in Python tests. Instead of hitting real external services, responses returns predefined mock responses — your suite stops being flaky because every call gets the same canned answer, in milliseconds, with no network involved.

When to Use

  • Testing code that calls third-party REST APIs (payment gateways, email services, SMS providers)
  • Simulating API error responses (500s, timeouts, rate limits) without a real service
  • Verifying that your code sends the correct request body, headers, and query parameters
  • Running tests in CI without network access or API keys

When NOT to Use

  • Testing your own API endpoints — use a test client (e.g., FastAPI TestClient, Django TestCase)
  • Integration tests that need a real database or message queue — use Testcontainers instead
  • Load testing — mocks don’t reflect real-world latency or throughput
  • Testing webhook receivers or flows that need a standalone stub server — use WireMock, MSW, or API mocking tools in general

Solution

Setup

pip install responses pytest

Basic mock response

import responses
import pytest
import requests

@responses.activate
def test_get_user():
    responses.add(
        method=responses.GET,
        url="https://api.example.com/users/1",
        json={"id": 1, "name": "Alice", "email": "alice@example.com"},
        status=200,
    )

    resp = requests.get("https://api.example.com/users/1")
    assert resp.status_code == 200
    assert resp.json()["name"] == "Alice"

Mock with headers and query params

import responses
import requests
from responses import matchers

@responses.activate
def test_search_with_params():
    responses.add(
        method=responses.GET,
        url="https://api.example.com/search",
        json={"results": [{"title": "Python Guide"}]},
        status=200,
        headers={"X-RateLimit-Remaining": "99"},
        match=[matchers.query_param_matcher({"q": "python"})],
    )

    resp = requests.get(
        "https://api.example.com/search",
        params={"q": "python", "page": 1},
        headers={"Authorization": "Bearer test-token"},
    )

    assert resp.headers["X-RateLimit-Remaining"] == "99"
    assert resp.json()["results"][0]["title"] == "Python Guide"

    # Verify the request was made correctly
    assert len(responses.calls) == 1
    assert "q=python" in responses.calls[0].request.url

Mock error responses

import responses
import pytest
import requests

@responses.activate
def test_handle_server_error():
    responses.add(
        method=responses.GET,
        url="https://api.example.com/users/1",
        status=500,
        json={"error": "Internal Server Error"},
    )

    with pytest.raises(requests.HTTPError):
        resp = requests.get("https://api.example.com/users/1")
        resp.raise_for_status()


@responses.activate
def test_handle_timeout():
    responses.add(
        method=responses.GET,
        url="https://api.example.com/slow-endpoint",
        body=requests.exceptions.Timeout("Connection timed out"),
    )

    with pytest.raises(requests.exceptions.Timeout):
        requests.get("https://api.example.com/slow-endpoint")

Mock multiple responses for the same URL

import responses
import requests

@responses.activate
def test_retry_logic():
    responses.add(
        method=responses.GET,
        url="https://api.example.com/data",
        status=503,
    )
    responses.add(
        method=responses.GET,
        url="https://api.example.com/data",
        status=200,
        json={"data": "success"},
    )

    # First call fails
    resp1 = requests.get("https://api.example.com/data")
    assert resp1.status_code == 503

    # Second call succeeds
    resp2 = requests.get("https://api.example.com/data")
    assert resp2.status_code == 200
    assert resp2.json()["data"] == "success"

Using a callback for dynamic responses

import json
import responses
import requests

@responses.activate
def test_callback_response():
    def request_callback(request):
        payload = request.body
        if b"premium" in payload:
            return (200, {}, json.dumps({"tier": "premium", "quota": 10000}))
        return (200, {}, json.dumps({"tier": "free", "quota": 100}))

    responses.add_callback(
        method=responses.POST,
        url="https://api.example.com/subscribe",
        callback=request_callback,
        content_type="application/json",
    )

    resp = requests.post(
        "https://api.example.com/subscribe",
        json={"plan": "premium"},
    )
    assert resp.json()["quota"] == 10000

Asserting on request body

import json
import responses
import requests

@responses.activate
def test_post_request_body():
    responses.add(
        method=responses.POST,
        url="https://api.example.com/orders",
        status=201,
        json={"id": 42, "status": "created"},
    )

    requests.post(
        "https://api.example.com/orders",
        json={"product_id": 10, "quantity": 3},
    )

    sent_body = json.loads(responses.calls[0].request.body)
    assert sent_body["product_id"] == 10
    assert sent_body["quantity"] == 3

Using responses as a pytest fixture

import responses
import pytest
import requests

@pytest.fixture
def mock_api():
    with responses.RequestsMock() as rsps:
        yield rsps

def test_with_fixture(mock_api):
    mock_api.add(
        method=responses.GET,
        url="https://api.example.com/health",
        status=200,
        json={"status": "healthy"},
    )

    resp = requests.get("https://api.example.com/health")
    assert resp.json()["status"] == "healthy"

Runnable suite: every test on this page ships in the python-mock-external-apis-responses companion repo, including the conftest.py fixture.

Explanation

responses works by monkey-patching requests’ transport adapter. When a test decorated with @responses.activate (or running inside responses.RequestsMock()) calls requests.get, the call never reaches the socket — responses looks up the registered mocks and returns the first one that matches.

Matching happens in two stages: the URL (exact string or re.compile pattern) and the HTTP method, then any optional match list of matchers. A registered response that matches on URL but fails a matcher is skipped, not failed — responses keeps searching the registry, and if nothing matches at all the request raises ConnectionError, which is how forgotten mocks surface.

Two operational details matter in practice. First, registered responses for the same URL are consumed in order — that’s what powers the retry-sequence example above. Second, the decorator resets the registry on every test; if you manage RequestsMock yourself through a fixture, you control when mocks are added and when assert_all_requests_are_fired applies.

flowchart diagram: test calls requests.get

Variants

Using httpx with respx

If you use httpx instead of requests, use the respx library:

import respx
import httpx

@respx.mock
def test_httpx_mock():
    respx.get("https://api.example.com/users/1").respond(
        200, json={"id": 1, "name": "Alice"}
    )

    resp = httpx.get("https://api.example.com/users/1")
    assert resp.json()["name"] == "Alice"

Using aioresponses for aiohttp

from aioresponses import aioresponses
import aiohttp
import pytest

@pytest.mark.asyncio
async def test_async_mock():
    with aioresponses() as m:
        m.get("https://api.example.com/data", payload={"key": "value"})

        async with aiohttp.ClientSession() as session:
            async with session.get("https://api.example.com/data") as resp:
                data = await resp.json()
                assert data["key"] == "value"

Best Practices

  • Always use @responses.activate or the context manager — without it, real HTTP calls go through
  • Assert on responses.calls to verify your code sent the correct request
  • Use add_callback for logic a static response can’t express, like branching on the request body
  • Reset responses.calls between tests if you’re checking call counts
  • Mock at the HTTP layer, not at the function layer — this tests the real integration path

Common Mistakes

  • Forgetting @responses.activate: without it, responses.add raises an error or real requests go through.
  • Relying on exact URL matching: responses matches URLs exactly by default, so a different query string silently misses your mock. Use matchers.query_param_matcher (or query_string_matcher) in the match list — match_querystring was deprecated in responses 0.17.0 — or a regex URL when the path varies.
  • Mocking too many endpoints: if every test mocks 10 endpoints, the test setup becomes the test. Extract shared mocks into fixtures.
  • Not testing error paths: mock 500s, timeouts, and rate limits — these are the paths that fail in production.
  • Using mocks for integration tests: mocks prove your parsing and error handling — they say nothing about whether the real API still works. For that, you need contract tests against the real service.

Frequently Asked Questions

How do I match URLs with regex?
import re
import responses

responses.add(
    method=responses.GET,
    url=re.compile(r"https://api\.example\.com/users/\d+"),
    json={"id": 1},
)
How do I match on query parameters or headers?

Use the match list with helpers from responses.matchers:

from responses import matchers

responses.add(
    method=responses.GET,
    url="https://api.example.com/search",
    json={"results": []},
    match=[
        matchers.query_param_matcher({"q": "python"}),
        matchers.header_matcher({"Authorization": "Bearer test-token"}),
    ],
)

Avoid match_querystring=True — it was deprecated in responses 0.17.0.

How do I simulate a connection error?
import responses
import requests

responses.add(
    method=responses.GET,
    url="https://api.example.com/down",
    body=requests.exceptions.ConnectionError("Connection refused"),
)
Can I mock streaming responses?

Yes, pass a generator to body:

import responses

def stream_generator():
    yield b"chunk1\n"
    yield b"chunk2\n"

responses.add(
    method=responses.GET,
    url="https://api.example.com/stream",
    body=stream_generator(),
    content_type="text/event-stream",
)
How do I verify how many times an endpoint was called?
assert len(responses.calls) == 3
# Or filter by URL
api_calls = [c for c in responses.calls if "api.example.com" in c.request.url]
assert len(api_calls) == 3
Should I use responses or unittest.mock.patch?

Use responses when your code uses the requests library. Reach for unittest.mock.patch when you need to fake a function or method rather than an HTTP call. responses exercises the real HTTP call path — headers, retries, raise_for_status — so it's the more realistic of the two.