Visión General
La librería responses intercepta las peticiones HTTP hechas por la librería requests en tests de Python. En lugar de contactar servicios externos reales, responses devuelve respuestas mock predefinidas — tu suite deja de ser flaky porque cada llamada recibe la misma respuesta prefabricada, en milisegundos, sin red de por medio.
Cuándo Usarlo
- Testear código que llama APIs REST de terceros (payment gateways, servicios de email, proveedores de SMS)
- Simular respuestas de error de API (500s, timeouts, rate limits) sin un servicio real
- Verificar que tu código envía el body, headers y query parameters correctos
- Correr tests en CI sin acceso a red ni API keys
Cuándo NO Usarlo
- Testear tus propios endpoints — usa un test client (e.g., FastAPI
TestClient, DjangoTestCase) - Tests de integración que necesitan una base de datos real o message queue — usa Testcontainers
- Load testing — los mocks no reflejan latencia ni throughput del mundo real
- Testear webhook receivers o flujos que necesitan un servidor stub independiente — usa WireMock, MSW o herramientas de API mocking en general
Solución
Setup
pip install responses pytest
Mock response básico
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 con headers y 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"
# Verificar que la petición se hizo correctamente
assert len(responses.calls) == 1
assert "q=python" in responses.calls[0].request.url
Mock de respuestas de error
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")
Múltiples respuestas para la misma 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"},
)
# Primera llamada falla
resp1 = requests.get("https://api.example.com/data")
assert resp1.status_code == 503
# Segunda llamada exitosa
resp2 = requests.get("https://api.example.com/data")
assert resp2.status_code == 200
assert resp2.json()["data"] == "success"
Callback para respuestas dinámicas
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
Verificar el body de la petición
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
Usar responses como fixture de pytest
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"
Suite ejecutable: todos los tests de esta página están en el repositorio complementario python-mock-external-apis-responses, incluido el fixture de conftest.py.
Explicación
responses funciona haciendo monkey-patch al transport adapter de requests. Cuando un test decorado con @responses.activate (o corriendo dentro de responses.RequestsMock()) llama a requests.get, la llamada nunca llega al socket — responses busca entre los mocks registrados y devuelve el primero que coincida.
El matching ocurre en dos etapas: primero la URL (string exacta o patrón re.compile) y el método HTTP, luego cualquier lista opcional match de matchers. Un mock registrado que coincide en URL pero falla un matcher se salta, no falla — responses sigue buscando en el registro, y si nada coincide la petición lanza ConnectionError, que es como se descubren los mocks olvidados.
Dos detalles operativos importan en la práctica. Primero, las respuestas registradas para la misma URL se consumen en orden — eso es lo que hace funcionar el ejemplo de retry de arriba. Segundo, el decorador resetea el registro en cada test; si gestionas RequestsMock tú mismo con un fixture, controlas cuándo se añaden mocks y cuándo aplica assert_all_requests_are_fired.
Variantes
Usar httpx con respx
Si usas httpx en lugar de requests, usa la librería respx:
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"
Usar aioresponses para 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"
Buenas Prácticas
- Siempre usa
@responses.activateo el context manager — sin eso, las llamadas HTTP reales pasan - Verifica
responses.callspara comprobar que tu código envió la petición correcta - Usa
add_callbackpara lógica que una respuesta estática no puede expresar, como decidir según el body del request - Resetea
responses.callsentre tests si estás verificando call counts - Mockea a nivel HTTP, no a nivel función — esto testea el path de integración real
Errores Comunes
- Olvidar
@responses.activate: sin eso,responses.addlanza un error o las peticiones reales pasan. - Confiar en el match exacto de URL:
responsesmatchea URLs exactamente por defecto, así que un query string distinto falla tu mock en silencio. Usamatchers.query_param_matcher(oquery_string_matcher) en la listamatch—match_querystringse deprecó en responses 0.17.0 — o una URL regex cuando el path varía. - Mockear demasiados endpoints: si cada test mockea 10 endpoints, el setup del test se vuelve el test. Extrae mocks compartidos en fixtures.
- No testear paths de error: mockea 500s, timeouts y rate limits — estos son los paths que fallan en producción.
- Usar mocks para tests de integración: los mocks prueban tu parsing y manejo de errores — no dicen nada sobre si la API real sigue funcionando. Para eso necesitas contract tests contra el servicio real.
Preguntas frecuentes
¿Cómo hago match de URLs con regex?
import re
import responses
responses.add(
method=responses.GET,
url=re.compile(r"https://api\.example\.com/users/\d+"),
json={"id": 1},
)
¿Cómo hago match por query parameters o headers?
Usa la lista match con los helpers de 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"}),
],
)
Evita match_querystring=True — se deprecó en responses 0.17.0.
¿Cómo simulo un error de conexión?
import responses
import requests
responses.add(
method=responses.GET,
url="https://api.example.com/down",
body=requests.exceptions.ConnectionError("Connection refused"),
)
¿Puedo mockear streaming responses?
Sí, pasa un generador a 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",
)
¿Cómo verifico cuántas veces se llamó un endpoint?
assert len(responses.calls) == 3
# O filtra por URL
api_calls = [c for c in responses.calls if "api.example.com" in c.request.url]
assert len(api_calls) == 3
¿Debería usar responses o unittest.mock.patch?
Usa responses cuando tu código usa la librería requests. Usa unittest.mock.patch cuando necesitas falsear una función o método en lugar de una llamada HTTP. responses ejercita el path HTTP real — headers, retries, raise_for_status — así que es el más realista de los dos.
Recursos Relacionados
Pytest Fixtures y Parametrize
Cómo usar pytest fixtures y @pytest.mark.parametrize para escribir tests data-driven con lógica de setup reutilizable en proyectos Python.
RecipeEscribir Unit Tests con Mocks y Stubs
Cómo aislar código bajo test usando objetos mock, stubs y spies para reemplazar dependencias externas como bases de datos, APIs y sistemas de archivos.
RecipeMedir y Exigir Cobertura de Tests con pytest-cov
Mide y exige cobertura con pytest-cov: reportes HTML, branch coverage, exclusiones e integración.
RecipeProperty-Based Testing con Hypothesis
Cómo usar Hypothesis para property-based testing en Python, generando cientos de casos de test automáticamente desde strategies en lugar de escribirlos a mano.
RecipeMockear Peticiones de Red con MSW
Cómo usar Mock Service Worker (MSW) para interceptar peticiones de red en tests y desarrollo de JavaScript, incluyendo mocking de REST y GraphQL.
RecipeAPI Mocking para Testing
Construye tests confiables mockeando APIs externas con WireMock, MockServer y MSW para eliminar flakiness y testear casos edge.