Módulo 6: Mocking, Fixtures y Validation Loops
Mocking Servicios Externos en Práctica
Mocking Servicios Externos en Práctica
Descripción de la cápsula
Ya conoces los fundamentos de mocking — Mock, MagicMock, patch, side_effect. Pero en el mundo real no mockeas "objetos abstractos": mockeas APIs HTTP que devuelven JSON, clientes de OpenAI que generan texto, bases de datos que retornan filas, archivos que contienen config, variables de entorno que guardan API keys. Esta cápsula te lleva a la práctica concreta: cómo mockear los servicios externos más comunes en aplicaciones Python, con ejemplos ejecutables de principio a fin.
Al final dominarás el mock de HTTP, AI APIs, bases de datos, filesystem, tiempo y variables de entorno. También verás cómo generar mocks con Claude Code para acelerar el proceso. Cerrarás con un ejemplo completo: un servicio de clima que consume una API externa, testeado al 100% con mocks.
1. Mocking de APIs HTTP (requests / httpx)
El escenario típico
Tu código llama a una API externa para enviar notificaciones, obtener datos, o sincronizar con un servicio tercero. En tests no quieres hacer llamadas reales: son lentas, dependen de la red, pueden fallar por rate limits, y cuestan dinero si la API es de pago.
Con requests
# mymodule.py
import requests
def send_notification(email: str, message: str) -> dict:
"""Envía una notificación vía API externa."""
response = requests.post(
"https://api.notify.example.com/send",
json={"email": email, "message": message},
timeout=5,
)
response.raise_for_status()
return response.json()
Test con @patch:
# tests/test_notifications.py
from unittest.mock import patch
from mymodule import send_notification
@patch("mymodule.requests.post")
def test_send_notification_success(mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"sent": True}
mock_post.return_value.raise_for_status = lambda: None
result = send_notification("user@test.com", "Hello!")
assert result["sent"] is True
mock_post.assert_called_once()
call_args = mock_post.call_args
assert call_args[1]["json"]["email"] == "user@test.com"
assert call_args[1]["json"]["message"] == "Hello!"
Mock de respuestas de error y timeout
from unittest.mock import patch
import pytest
import requests
from mymodule import send_notification
@patch("mymodule.requests.post")
def test_send_notification_api_error(mock_post):
mock_post.return_value.raise_for_status.side_effect = requests.HTTPError("500")
mock_post.return_value.status_code = 500
with pytest.raises(requests.HTTPError):
send_notification("user@test.com", "Hello!")
@patch("mymodule.requests.post")
def test_send_notification_timeout(mock_post):
import requests as req
mock_post.side_effect = req.exceptions.Timeout("Connection timed out")
with pytest.raises(req.exceptions.Timeout):
send_notification("user@test.com", "Hello!")
Con httpx (async y sync)
Si usas httpx en vez de requests, el patching es similar:
# services/external.py
import httpx
def fetch_user(user_id: int) -> dict:
response = httpx.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
from unittest.mock import Mock
from services.external import fetch_user
@patch("services.external.httpx.get")
def test_fetch_user(mock_get):
mock_response = Mock()
mock_response.raise_for_status = Mock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mock_get.return_value = mock_response
result = fetch_user(1)
assert result["name"] == "Alice"
mock_get.assert_called_once_with("https://api.example.com/users/1")
Usando la librería responses (alternativa más realista)
La librería responses intercepta HTTP a nivel de socket y simula respuestas de forma más cercana a lo real:
# pip install responses
import responses
from mymodule import send_notification
@responses.activate
def test_send_notification_with_responses():
responses.add(
responses.POST,
"https://api.notify.example.com/send",
json={"sent": True},
status=200,
)
result = send_notification("user@test.com", "Hello!")
assert result["sent"] is True
assert len(responses.calls) == 1
assert responses.calls[0].request.url == "https://api.notify.example.com/send"
Con responses no necesitas patchar — la librería intercepta las llamadas HTTP reales y las redirige a tus respuestas mockeadas. Útil cuando el código usa requests internamente y quieres verificar que se llama la URL correcta con los headers correctos.
2. Mocking de APIs de AI (OpenAI / Anthropic)
Por qué las APIs de AI DEBEN mockearse
Las APIs de OpenAI, Anthropic, etc. no deberían llamarse en tests por tres razones:
- Costo: Cada llamada consume créditos. Miles de tests = factura elevada.
- No determinismo: El modelo puede devolver respuestas diferentes para el mismo prompt.
- Rate limits y latencia: Tests lentos y posibles fallos intermitentes.
Ejemplo con OpenAI (cliente moderno)
# services/summarizer.py
from openai import OpenAI
def generate_summary(client: OpenAI, text: str, max_tokens: int = 100) -> str:
"""Genera un resumen usando la API de OpenAI."""
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": f"Summarize in at most {max_tokens} words: {text}"}],
max_tokens=max_tokens,
)
return response.choices[0].message.content.strip()
Test con mock:
# tests/test_summarizer.py
from unittest.mock import Mock, patch
from services.summarizer import generate_summary
def test_generate_summary_returns_content():
mock_client = Mock()
mock_client.chat.completions.create.return_value = Mock(
choices=[Mock(message=Mock(content="This is a summary."))]
)
mock_client.chat.completions.create.return_value.choices[0].message.strip.return_value = "This is a summary."
result = generate_summary(mock_client, "Long text here...")
assert "summary" in result.lower()
mock_client.chat.completions.create.assert_called_once()
Si el cliente se importa internamente
# services/summarizer.py
from openai import OpenAI
_client = OpenAI(api_key="sk-...")
def generate_summary(text: str) -> str:
response = _client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": f"Summarize: {text}"}],
max_tokens=100,
)
return response.choices[0].message.content.strip()
@patch("services.summarizer._client")
def test_generate_summary(mock_client):
mock_response = Mock()
mock_response.choices = [Mock()]
mock_response.choices[0].message = Mock()
mock_response.choices[0].message.content = "Short summary."
mock_response.choices[0].message.strip.return_value = "Short summary."
mock_client.chat.completions.create.return_value = mock_response
result = generate_summary("Long text...")
assert "Short" in result
Patcha donde se usa (services.summarizer._client), no donde se define (openai.OpenAI).
Con Anthropic ( Claude )
La estructura es similar: mockeas el cliente o el método create:
# Con Anthropic
@patch("services.claude_wrapper.anthropic.Anthropic")
def test_generate_with_claude(mock_anthropic):
mock_client = Mock()
mock_client.messages.create.return_value = Mock(
content=[Mock(text="Mocked response.")]
)
mock_anthropic.return_value = mock_client
result = generate_with_claude("prompt")
assert "Mocked" in result
3. Mocking de operaciones de base de datos
Patrón: mockear la capa de acceso
No mockeas SQLAlchemy ni el driver directamente — mockeas la función o clase que encapsula las queries (repository, dao, etc.).
# mymodule.py
def get_user_by_email(db, email: str) -> dict | None:
"""Obtiene un usuario por email desde la base de datos."""
rows = db.query("SELECT * FROM users WHERE email = %s", (email,))
if not rows:
return None
return rows[0]
# tests/test_users.py
import pytest
from mymodule import get_user_by_email
def test_get_user_by_email_found(mocker):
mock_db = mocker.patch("mymodule.db")
mock_db.query.return_value = [{"id": 1, "email": "user@test.com", "name": "Test"}]
user = get_user_by_email(mock_db, "user@test.com")
assert user["email"] == "user@test.com"
mock_db.query.assert_called_once()
def test_get_user_by_email_not_found(mocker):
mock_db = mocker.patch("mymodule.db")
mock_db.query.return_value = []
user = get_user_by_email(mock_db, "nonexistent@test.com")
assert user is None
Si db es inyectado como parámetro, no hace falta patchar — pasas el mock directamente. Si db es un objeto global importado, usas mocker.patch("mymodule.db") o @patch("mymodule.db").
Con SQLAlchemy session
# repositories/user_repo.py
from sqlalchemy.orm import Session
def get_user(session: Session, user_id: int):
return session.query(User).filter(User.id == user_id).first()
def test_get_user(mocker):
mock_session = mocker.MagicMock()
mock_user = Mock(id=1, email="user@test.com")
mock_session.query.return_value.filter.return_value.first.return_value = mock_user
result = get_user(mock_session, 1)
assert result.email == "user@test.com"
4. Mocking del filesystem
mock_open para leer archivos
# config_loader.py
import json
def read_config(path: str) -> dict:
with open(path, "r") as f:
return json.load(f)
# tests/test_config_loader.py
from unittest.mock import mock_open, patch
from config_loader import read_config
def test_read_config(mocker):
mock_file = mock_open(read_data='{"key": "value", "port": 8080}')
mocker.patch("builtins.open", mock_file)
config = read_config("config.json")
assert config["key"] == "value"
assert config["port"] == 8080
mock_file.assert_called_once_with("config.json", "r")
Con unittest.mock (sin pytest-mock)
from unittest.mock import mock_open, patch
@patch("builtins.open", mock_open(read_data='{"key": "value"}'))
def test_read_config():
config = read_config("config.json")
assert config["key"] == "value"
Importante: Patchea builtins.open porque open es una función built-in. El path depende del módulo donde se usa: si config_loader importa y usa open, puedes usar config_loader.open si hicieras from builtins import open — pero lo más simple es builtins.open o el módulo que contiene la función bajo test.
Para asegurarte, patcha en el módulo que usa open:
@patch("config_loader.open", mock_open(read_data='{"key": "value"}'))
def test_read_config():
config = read_config("config.json")
assert config["key"] == "value"
5. Mocking de time y datetime
datetime.utcnow
# auth/utils.py
from datetime import datetime
def is_expired(token: dict) -> bool:
"""Verifica si un token JWT ha expirado."""
exp_str = token.get("exp")
if not exp_str:
return True
exp = datetime.fromisoformat(exp_str)
return datetime.utcnow() >= exp
# tests/test_auth.py
from unittest.mock import patch
from datetime import datetime
from auth.utils import is_expired
@patch("auth.utils.datetime")
def test_is_expired_true(mock_dt):
mock_dt.utcnow.return_value = datetime(2024, 6, 15, 12, 0, 0)
mock_dt.fromisoformat = datetime.fromisoformat
token = {"exp": "2024-06-15T11:00:00"}
assert is_expired(token) is True
@patch("auth.utils.datetime")
def test_is_expired_false(mock_dt):
mock_dt.utcnow.return_value = datetime(2024, 6, 15, 10, 0, 0)
mock_dt.fromisoformat = datetime.fromisoformat
token = {"exp": "2024-06-15T12:00:00"}
assert is_expired(token) is False
Si mockeas todo el módulo datetime, debes preservar fromisoformat porque tu código lo usa. Alternativamente, patcha solo utcnow:
@patch("auth.utils.datetime.utcnow")
def test_is_expired(mock_utcnow):
mock_utcnow.return_value = datetime(2024, 6, 15, 12, 0, 0)
token = {"exp": "2024-06-15T11:00:00"}
assert is_expired(token) is True
6. Mocking de variables de entorno
# config.py
import os
def get_api_key() -> str:
return os.environ.get("API_KEY", "")
def test_get_api_key(mocker):
mocker.patch.dict("os.environ", {"API_KEY": "test-key-123"}, clear=False)
assert get_api_key() == "test-key-123"
O con patch:
from unittest.mock import patch
@patch.dict("os.environ", {"API_KEY": "test-key-123"})
def test_get_api_key():
from config import get_api_key
assert get_api_key() == "test-key-123"
7. Generar mocks con Claude Code
Prompts efectivos
Puedes pedirle a Claude Code que genere mocks para un servicio concreto:
Genera mocks para testear esta función que llama a la API de clima. Incluye:
- Mock de respuesta exitosa (200, JSON con temp y condition)
- Mock de error 500
- Mock de timeout
- Tests usando pytest y unittest.mock
O más específico:
Necesito tests para
send_notificationque usarequests.post. Genera:
- test_send_notification_success — mock retorna {"sent": true}
- test_send_notification_api_error — mock lanza HTTPError
- test_send_notification_timeout — mock lanza Timeout Usa @patch en el módulo donde se usa requests.
Claude Code suele generar tests con la estructura correcta. Revisa que el path del patch sea el adecuado (donde se usa, no donde se define).
8. Ejemplo completo: Weather Service
Un servicio de clima que consume una API externa, con tests completos usando mocks.
Código del servicio
# weather_service.py
"""
Servicio de clima que consume una API externa.
"""
import os
import httpx
from typing import Any
WEATHER_API_URL = "https://api.weather.example.com/v1/current"
WEATHER_API_KEY = os.environ.get("WEATHER_API_KEY", "")
def get_current_weather(city: str) -> dict[str, Any]:
"""
Obtiene el clima actual para una ciudad.
Returns:
dict con keys: temp_c, condition, humidity, error (opcional)
"""
if not WEATHER_API_KEY:
return {"error": "API key not configured"}
try:
response = httpx.get(
f"{WEATHER_API_URL}",
params={"q": city, "key": WEATHER_API_KEY},
timeout=10,
)
response.raise_for_status()
data = response.json()
return {
"temp_c": data["current"]["temp_c"],
"condition": data["current"]["condition"]["text"],
"humidity": data["current"]["humidity"],
}
except httpx.HTTPStatusError as e:
return {"error": f"HTTP error: {e.response.status_code}"}
except httpx.TimeoutException:
return {"error": "Request timeout"}
except Exception as e:
return {"error": str(e)}
Tests completos
# tests/test_weather_service.py
"""
Tests del servicio de clima usando mocks.
"""
import pytest
from unittest.mock import patch, Mock
# Importar después de posible patch de os.environ
import weather_service
@patch.dict("os.environ", {"WEATHER_API_KEY": "test-key-123"}, clear=False)
@patch("weather_service.httpx.get")
def test_get_current_weather_success(mock_get, monkeypatch):
monkeypatch.setattr(weather_service, "WEATHER_API_KEY", "test-key-123")
mock_response = Mock()
mock_response.json.return_value = {
"current": {
"temp_c": 22.5,
"condition": {"text": "Partly cloudy"},
"humidity": 65,
}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = weather_service.get_current_weather("Madrid")
assert result["temp_c"] == 22.5
assert result["condition"] == "Partly cloudy"
assert result["humidity"] == 65
assert "error" not in result
mock_get.assert_called_once()
@patch.dict("os.environ", {"WEATHER_API_KEY": "test-key"}, clear=False)
@patch("weather_service.httpx.get")
def test_get_current_weather_http_error(mock_get, monkeypatch):
monkeypatch.setattr(weather_service, "WEATHER_API_KEY", "test-key")
mock_response = Mock()
mock_response.status_code = 500
mock_get.return_value = mock_response
mock_get.return_value.raise_for_status.side_effect = Exception("500")
import httpx
mock_get.return_value.raise_for_status.side_effect = httpx.HTTPStatusError(
"Server Error", request=Mock(), response=Mock(status_code=500)
)
result = weather_service.get_current_weather("Madrid")
assert "error" in result
assert "500" in result["error"]
@patch.dict("os.environ", {"WEATHER_API_KEY": "test-key"}, clear=False)
@patch("weather_service.httpx.get")
def test_get_current_weather_timeout(mock_get, monkeypatch):
monkeypatch.setattr(weather_service, "WEATHER_API_KEY", "test-key")
import httpx
mock_get.side_effect = httpx.TimeoutException("Connection timed out")
result = weather_service.get_current_weather("Madrid")
assert result["error"] == "Request timeout"
@patch.dict("os.environ", {}, clear=False)
def test_get_current_weather_no_api_key(monkeypatch):
monkeypatch.setattr(weather_service, "WEATHER_API_KEY", "")
result = weather_service.get_current_weather("Madrid")
assert result["error"] == "API key not configured"
Versión simplificada que evita problemas con el import del módulo y las variables de entorno:
# tests/test_weather_service.py - versión simplificada
import pytest
from unittest.mock import patch, Mock
import httpx
@patch.dict("os.environ", {"WEATHER_API_KEY": "test-key-123"})
@patch("weather_service.httpx.get")
def test_weather_success(mock_get):
import importlib
import weather_service
importlib.reload(weather_service)
mock_get.return_value = Mock(
json=lambda: {"current": {"temp_c": 22, "condition": {"text": "Sunny"}, "humidity": 50}},
raise_for_status=Mock(),
)
result = weather_service.get_current_weather("Madrid")
assert result["temp_c"] == 22
assert result["condition"] == "Sunny"
Una implementación más limpia usa inyección de dependencias para el cliente HTTP y la API key:
# weather_service_v2.py - con inyección de dependencias
from typing import Any, Callable
def get_current_weather(
city: str,
fetch_weather: Callable[[str], dict],
api_key: str = "",
) -> dict[str, Any]:
if not api_key:
return {"error": "API key not configured"}
try:
data = fetch_weather(city)
return {
"temp_c": data["current"]["temp_c"],
"condition": data["current"]["condition"]["text"],
"humidity": data["current"]["humidity"],
}
except Exception as e:
return {"error": str(e)}
def test_weather_v2_success():
def mock_fetch(city):
return {
"current": {"temp_c": 20, "condition": {"text": "Cloudy"}, "humidity": 70}
}
result = get_current_weather("Barcelona", fetch_weather=mock_fetch, api_key="key")
assert result["temp_c"] == 20
assert result["condition"] == "Cloudy"
La inyección simplifica los tests: no necesitas patchar, solo pasas un mock o una función fake.
Ejercicios
Ejercicio 1: Mock de HTTP con respuestas (Fácil)
Tienes una función fetch_products() que hace requests.get("https://api.example.com/products") y retorna response.json(). Escribe un test que mockee la llamada y verifique que retorna una lista con al menos un producto {"id": 1, "name": "Widget"}.
Ver solución
from unittest.mock import patch
from mymodule import fetch_products
@patch("mymodule.requests.get")
def test_fetch_products_returns_list(mock_get):
mock_get.return_value.json.return_value = [{"id": 1, "name": "Widget"}]
mock_get.return_value.raise_for_status = lambda: None
result = fetch_products()
assert len(result) >= 1
assert result[0]["name"] == "Widget"
mock_get.assert_called_once_with("https://api.example.com/products")
Ejercicio 2: Mock de OpenAI (Medio)
Una función classify_sentiment(text: str, client: OpenAI) -> str llama a client.chat.completions.create() y retorna choices[0].message.content. Escribe un test que pase un mock del cliente y verifique que retorna "positive".
Ver solución
from unittest.mock import Mock
from mymodule import classify_sentiment
def test_classify_sentiment_returns_positive():
mock_client = Mock()
mock_response = Mock()
mock_response.choices = [Mock()]
mock_response.choices[0].message = Mock()
mock_response.choices[0].message.content = "positive"
mock_client.chat.completions.create.return_value = mock_response
result = classify_sentiment("I love this!", mock_client)
assert result == "positive"
Ejercicio 3: Mock de base de datos (Medio)
Una función get_user_by_id(repo, user_id: int) llama a repo.get(user_id) y retorna el usuario o None. Escribe dos tests: uno donde el repo retorna un usuario y otro donde retorna None.
Ver solución
from unittest.mock import Mock
from mymodule import get_user_by_id
def test_get_user_by_id_found():
mock_repo = Mock()
mock_repo.get.return_value = {"id": 1, "name": "Alice"}
user = get_user_by_id(mock_repo, 1)
assert user["name"] == "Alice"
mock_repo.get.assert_called_once_with(1)
def test_get_user_by_id_not_found():
mock_repo = Mock()
mock_repo.get.return_value = None
user = get_user_by_id(mock_repo, 999)
assert user is None
Ejercicio 4: Mock de archivo con mock_open (Medio)
Una función load_secrets(path: str) -> dict lee un JSON del filesystem. Escribe un test que mockee open y verifique que retorna {"db_password": "secret123"}.
Ver solución
from unittest.mock import mock_open, patch
from mymodule import load_secrets
@patch("mymodule.open", mock_open(read_data='{"db_password": "secret123"}'))
def test_load_secrets():
result = load_secrets("/etc/secrets.json")
assert result["db_password"] == "secret123"
Si load_secrets está en mymodule, patcha mymodule.open. Si usa open directamente, patcha builtins.open.
Ejercicio 5: Mock de datetime (Medio)
Una función is_weekend() -> bool usa datetime.now().weekday() y retorna True si es sábado (5) o domingo (6). Escribe un test que mockee datetime para simular un sábado y verifique que retorna True.
Ver solución
from unittest.mock import patch
from datetime import datetime
@patch("mymodule.datetime")
def test_is_weekend_saturday(mock_dt):
mock_dt.now.return_value = datetime(2024, 6, 15) # Sábado
mock_dt.now.return_value.weekday = lambda: 5
from mymodule import is_weekend
assert is_weekend() is True
O creando un datetime real con weekday 5:
@patch("mymodule.datetime")
def test_is_weekend_saturday(mock_dt):
saturday = datetime(2024, 6, 15)
mock_dt.now.return_value = saturday
from mymodule import is_weekend
assert is_weekend() is True
Ejercicio 6: Mock de variable de entorno (Fácil)
Una función get_database_url() -> str retorna os.environ.get("DATABASE_URL", "sqlite:///default.db"). Escribe un test que establezca DATABASE_URL=postgres://localhost/test y verifique que la función retorna esa URL.
Ver solución
from unittest.mock import patch
from mymodule import get_database_url
@patch.dict("os.environ", {"DATABASE_URL": "postgres://localhost/test"})
def test_get_database_url():
assert get_database_url() == "postgres://localhost/test"
Con pytest-mock:
def test_get_database_url(mocker):
mocker.patch.dict("os.environ", {"DATABASE_URL": "postgres://localhost/test"})
from mymodule import get_database_url
assert get_database_url() == "postgres://localhost/test"
Troubleshooting
Problema 1: patch no reemplaza la llamada — sigue usando el servicio real
Causa: Patchas en el namespace incorrecto. Si mymodule hace from requests import get y usa get(), debes patchar mymodule.get, no requests.get.
Solución: Siempre patcha donde el objeto está usado. Si tienes dudas, busca el import en el archivo bajo test y patcha ese namespace.
Problema 2: mock de OpenAI/Anthropic — AttributeError en choices[0].message
Causa: El mock no tiene la estructura anidada que el código espera. Mock() crea mocks automáticos para atributos, pero a veces .strip() o accesos por índice fallan.
Solución: Construye la estructura explícitamente:
mock_response = Mock()
mock_message = Mock()
mock_message.content = "text"
mock_message.strip.return_value = "text"
mock_response.choices = [Mock(message=mock_message)]
mock_client.chat.completions.create.return_value = mock_response
Problema 3: mock_open no se aplica — sigue leyendo archivos reales
Causa: Patchas open en un módulo distinto al que lo usa. O usas mock_open incorrectamente (por ejemplo, sin pasar read_data).
Solución: Patcha en el módulo que abre el archivo. Asegúrate de que mock_open(read_data='...') recibe un string válido (ej. JSON si el código hace json.load).
Problema 4: variable de entorno no se refleja en el test
Causa: El módulo ya importó os.environ o la variable antes de que el patch se aplicara. Algunos módulos cachean os.environ.get("X") al importar.
Solución: Usa patch.dict antes de importar el módulo bajo test, o usa importlib.reload(module) después del patch. Mejor aún: inyecta la configuración como parámetro para evitar depender de os.environ en tiempo de importación.
Problema 5: test pasa de forma aislada pero falla cuando corre con otros tests
Causa: Un mock o patch persiste entre tests y contamina el estado. Por ejemplo, patch a nivel de módulo sin decorador o context manager.
Solución: Usa @patch como decorador o with patch(...): para que la restauración sea automática. Con pytest-mock, mocker.patch limpia al final del test.
Conexión con Proyecto
En el proyecto Validation pipeline del módulo 6:
- Mockearás la API de OpenAI (o similar) para simular respuestas de generación de texto
- Mockearás el servicio de base de datos para simular lecturas/escrituras sin PostgreSQL real
- Usarás las técnicas de esta cápsula: patch de HTTP, mock de clientes de AI, mock de repositorios
- El pipeline procesará datos de entrada, los enviará a los mocks, y los tests verificarán el flujo completo
La siguiente cápsula (Cápsula 06) es el proyecto del módulo: construir el validation pipeline con mocks, fixtures y validation loops. Esta cápsula te da las herramientas para mockear correctamente los servicios externos que el pipeline consume.
Resumen
- HTTP (requests/httpx): Usa
@patchsobre el cliente o el método (requests.get,httpx.get) en el namespace donde se usa. Configurareturn_value.json,raise_for_status, yside_effectpara errores y timeouts. - APIs de AI (OpenAI/Anthropic): Siempre mockear — coste, no determinismo y rate limits. Construye la estructura de respuesta (
choices[0].message.content) explícitamente. - Base de datos: Mockea la capa de acceso (repository, dao, session), no SQL directamente.
- Filesystem: Usa
mock_open(read_data='...')y patchaopenen el módulo que lo usa. - Tiempo: Patcha
datetimeodatetime.utcnowpara fijar el momento "actual". - Variables de entorno: Usa
patch.dict("os.environ", {...})omocker.patch.dict. - Claude Code: Puede generar mocks completos si le indicas el servicio y los escenarios (success, error, timeout).
- Inyección de dependencias: Facilita los tests — pasa mocks directamente en vez de patchar.
Recursos Adicionales
- unittest.mock — mock_open - Documentación de mock_open
- responses library - Mocking de HTTP más realista para requests
- pytest-mock - Fixture mocker para pytest
- Where to patch - Regla de oro del patching
- Testing HTTP with httpx - Documentación de httpx para testing
- VCR.py - Grabar y reproducir respuestas HTTP reales (alternativa a mocks manuales)
Módulo 6, Cápsula 05 — Testing with Claude Code Guide