Las llamadas HTTP son dependencias externas: pueden fallar, responder lento, cambiar datos o no estar disponibles. En pruebas unitarias normalmente queremos reemplazarlas por respuestas controladas.
En este tema veremos cómo mockear llamadas hechas con requests, cómo simular respuestas JSON, errores HTTP y timeouts, y cuándo conviene inyectar un cliente HTTP.
Supongamos que tenemos este código en clientes_api.py:
import requests
def obtener_cliente(cliente_id):
respuesta = requests.get(
f"https://api.example.com/clientes/{cliente_id}",
timeout=5,
)
respuesta.raise_for_status()
return respuesta.json()
Queremos probar la función sin consultar la API real.
Como el módulo usa requests.get, podemos parchear clientes_api.requests.get:
from unittest.mock import Mock, patch
from clientes_api import obtener_cliente
def test_obtener_cliente_exitoso():
respuesta = Mock()
respuesta.json.return_value = {"id": 1, "nombre": "Ana"}
respuesta.raise_for_status.return_value = None
with patch("clientes_api.requests.get", return_value=respuesta) as get_mock:
cliente = obtener_cliente(1)
assert cliente == {"id": 1, "nombre": "Ana"}
get_mock.assert_called_once_with(
"https://api.example.com/clientes/1",
timeout=5,
)
respuesta.raise_for_status.assert_called_once_with()
respuesta.json.assert_called_once_with()
La respuesta simulada implementa solo los métodos que el código usa.
Si prefieres evitar mocks anidados, puedes crear una respuesta stub:
class RespuestaHttpStub:
def __init__(self, datos):
self.datos = datos
def raise_for_status(self):
return None
def json(self):
return self.datos
Prueba:
def test_obtener_cliente_con_respuesta_stub():
respuesta = RespuestaHttpStub({"id": 1, "nombre": "Ana"})
with patch("clientes_api.requests.get", return_value=respuesta):
cliente = obtener_cliente(1)
assert cliente == {"id": 1, "nombre": "Ana"}
Esta opción puede ser más legible cuando la respuesta se reutiliza en muchas pruebas.
requests lanza errores como requests.HTTPError cuando raise_for_status detecta un código de error.
import requests
def test_obtener_cliente_error_http():
respuesta = Mock()
respuesta.raise_for_status.side_effect = requests.HTTPError("404")
with patch("clientes_api.requests.get", return_value=respuesta):
with pytest.raises(requests.HTTPError):
obtener_cliente(99)
La prueba simula un error sin depender de que la API real devuelva 404.
Muchas aplicaciones no quieren exponer errores de requests al resto del sistema. Podemos traducirlos:
import requests
class ClienteNoDisponibleError(Exception):
pass
def obtener_cliente(cliente_id):
try:
respuesta = requests.get(
f"https://api.example.com/clientes/{cliente_id}",
timeout=5,
)
respuesta.raise_for_status()
except requests.RequestException as error:
raise ClienteNoDisponibleError(
"No se pudo obtener el cliente"
) from error
return respuesta.json()
Ahora podemos probar que se lanza el error propio.
Prueba:
import pytest
import requests
from clientes_api import ClienteNoDisponibleError
def test_obtener_cliente_traduce_error_http():
respuesta = Mock()
respuesta.raise_for_status.side_effect = requests.HTTPError("500")
with patch("clientes_api.requests.get", return_value=respuesta):
with pytest.raises(ClienteNoDisponibleError):
obtener_cliente(1)
La prueba verifica el contrato de nuestra aplicación, no el detalle interno de requests.
Un timeout ocurre durante la llamada a requests.get:
def test_obtener_cliente_timeout():
with patch(
"clientes_api.requests.get",
side_effect=requests.Timeout("Tiempo agotado"),
):
with pytest.raises(ClienteNoDisponibleError):
obtener_cliente(1)
Aquí el mock de get lanza la excepción directamente.
Cuando el código envía parámetros, conviene verificarlos:
def buscar_clientes(nombre):
respuesta = requests.get(
"https://api.example.com/clientes",
params={"nombre": nombre},
timeout=5,
)
respuesta.raise_for_status()
return respuesta.json()
Prueba:
def test_buscar_clientes_verifica_params():
respuesta = Mock()
respuesta.raise_for_status.return_value = None
respuesta.json.return_value = [{"id": 1, "nombre": "Ana"}]
with patch("clientes_api.requests.get", return_value=respuesta) as get_mock:
clientes = buscar_clientes("Ana")
assert clientes == [{"id": 1, "nombre": "Ana"}]
get_mock.assert_called_once_with(
"https://api.example.com/clientes",
params={"nombre": "Ana"},
timeout=5,
)
Para enviar datos:
def crear_cliente(datos):
respuesta = requests.post(
"https://api.example.com/clientes",
json=datos,
timeout=5,
)
respuesta.raise_for_status()
return respuesta.json()
Prueba:
def test_crear_cliente():
respuesta = Mock()
respuesta.raise_for_status.return_value = None
respuesta.json.return_value = {"id": 10, "nombre": "Ana"}
datos = {"nombre": "Ana"}
with patch("clientes_api.requests.post", return_value=respuesta) as post_mock:
cliente = crear_cliente(datos)
assert cliente == {"id": 10, "nombre": "Ana"}
post_mock.assert_called_once_with(
"https://api.example.com/clientes",
json=datos,
timeout=5,
)
Otra opción es recibir el cliente HTTP como dependencia:
def obtener_cliente(cliente_id, cliente_http):
respuesta = cliente_http.get(
f"https://api.example.com/clientes/{cliente_id}",
timeout=5,
)
respuesta.raise_for_status()
return respuesta.json()
La prueba no necesita patch:
def test_obtener_cliente_con_cliente_inyectado():
cliente_http = Mock()
respuesta = Mock()
respuesta.raise_for_status.return_value = None
respuesta.json.return_value = {"id": 1, "nombre": "Ana"}
cliente_http.get.return_value = respuesta
cliente = obtener_cliente(1, cliente_http)
assert cliente == {"id": 1, "nombre": "Ana"}
También podemos crear un fake sencillo:
class ClienteHttpFake:
def __init__(self, respuestas):
self.respuestas = respuestas
self.llamadas = []
def get(self, url, **kwargs):
self.llamadas.append({"metodo": "GET", "url": url, "kwargs": kwargs})
return self.respuestas[url]
Este fake puede ser útil si varias pruebas consultan distintas URLs controladas.
Combinamos el fake con una respuesta simple:
class RespuestaFake:
def __init__(self, datos):
self.datos = datos
def raise_for_status(self):
return None
def json(self):
return self.datos
Uso:
def test_obtener_cliente_con_fake():
url = "https://api.example.com/clientes/1"
cliente_http = ClienteHttpFake({
url: RespuestaFake({"id": 1, "nombre": "Ana"})
})
cliente = obtener_cliente(1, cliente_http)
assert cliente == {"id": 1, "nombre": "Ana"}
assert cliente_http.llamadas[0]["url"] == url
Si una prueba verifica cada detalle interno de requests, puede volverse frágil. Verifica lo que sea parte del contrato: URL, método, parámetros importantes, timeout y resultado esperado.
No siempre hace falta verificar cada llamada a raise_for_status si el comportamiento final ya cubre el caso. Pero en clientes HTTP suele ser razonable comprobarlo porque omitirlo puede ocultar errores HTTP.
Una prueba unitaria no debería depender de internet. Las llamadas reales pueden fallar por red, límites de API, credenciales o cambios de datos.
Prueba esta función:
import requests
def obtener_cotizacion(moneda):
respuesta = requests.get(
"https://api.example.com/cotizaciones",
params={"moneda": moneda},
timeout=3,
)
respuesta.raise_for_status()
datos = respuesta.json()
return datos["valor"]
Escribe una prueba de respuesta exitosa y otra de timeout.
Una solución:
from unittest.mock import Mock, patch
import pytest
import requests
from cotizaciones import obtener_cotizacion
def test_obtener_cotizacion_exitosa():
respuesta = Mock()
respuesta.raise_for_status.return_value = None
respuesta.json.return_value = {"valor": 1000}
with patch("cotizaciones.requests.get", return_value=respuesta) as get_mock:
valor = obtener_cotizacion("USD")
assert valor == 1000
get_mock.assert_called_once_with(
"https://api.example.com/cotizaciones",
params={"moneda": "USD"},
timeout=3,
)
def test_obtener_cotizacion_timeout():
with patch(
"cotizaciones.requests.get",
side_effect=requests.Timeout("Tiempo agotado"),
):
with pytest.raises(requests.Timeout):
obtener_cotizacion("USD")
La prueba controla completamente la respuesta HTTP y el error de red.
Mockear llamadas HTTP permite probar clientes de APIs sin depender de internet. Podemos reemplazar requests.get o requests.post, simular respuestas JSON, errores HTTP y timeouts, y verificar parámetros importantes.
En el próximo tema veremos cómo probar envío de correos, notificaciones y colas de mensajes.