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_notification que usa requests.post. Genera:

  1. test_send_notification_success — mock retorna {"sent": true}
  2. test_send_notification_api_error — mock lanza HTTPError
  3. 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 @patch sobre el cliente o el método (requests.get, httpx.get) en el namespace donde se usa. Configura return_value.json, raise_for_status, y side_effect para 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 patcha open en el módulo que lo usa.
  • Tiempo: Patcha datetime o datetime.utcnow para fijar el momento "actual".
  • Variables de entorno: Usa patch.dict("os.environ", {...}) o mocker.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

  1. unittest.mock — mock_open - Documentación de mock_open
  2. responses library - Mocking de HTTP más realista para requests
  3. pytest-mock - Fixture mocker para pytest
  4. Where to patch - Regla de oro del patching
  5. Testing HTTP with httpx - Documentación de httpx para testing
  6. VCR.py - Grabar y reproducir respuestas HTTP reales (alternativa a mocks manuales)

Módulo 6, Cápsula 05 — Testing with Claude Code Guide