Módulo 6: Mocking, Fixtures y Validation Loops

Fundamentos de Mocking

Fundamentos de Mocking

Descripción de la cápsula

Tu función llama a la API de OpenAI. ¿Necesitas hacer una petición real en cada test? Tu endpoint consulta PostgreSQL. ¿Necesitas un servidor de base de datos corriendo para ejecutar pytest? Tu servicio genera archivos en disco. ¿Quieres que cada ejecución de tests escriba y borre archivos en tu sistema?

La respuesta es mocking: reemplazar dependencias externas con versiones controladas que simulan el comportamiento real. Un mock de la API de OpenAI retorna una respuesta predefinida en microsegundos. Un mock de la base de datos simula queries sin disco. Los tests se vuelven rápidos, deterministas y baratos.

Esta cápsula te enseña los fundamentos de mocking en Python: unittest.mock (Mock, MagicMock, patch, return_value, side_effect) y pytest-mock. Al terminar, podrás aislar código que depende de APIs, databases, filesystem o tiempo, y escribir tests que verifican el comportamiento de tu lógica sin depender del mundo real.


Por qué existe el mocking

El problema: código que depende de lo externo

Tu código suele depender de cosas que están fuera de tu control:

  • APIs externas: OpenAI, Stripe, Twitter, servicios de terceros
  • Bases de datos: PostgreSQL, Redis, MongoDB
  • Filesystem: leer/escribir archivos, directorios
  • Tiempo: datetime.now(), time.sleep() — tests que dependen del reloj son no-deterministas
  • Aleatoriedad: random.random() — cada ejecución da resultados distintos

Sin mocking, un test que llama a la API de OpenAI tarda segundos, consume créditos, y puede fallar si la API está caída o cambia su respuesta. Un test que usa la base de datos real requiere que PostgreSQL esté corriendo, con datos conocidos, y corre más lento. Los tests dejan de ser un feedback loop rápido y se convierten en un cuello de botella.

Los tres principios de los tests bien escritos

Para que los tests cumplan su función, necesitan ser:

  1. Rápidos: ejecutarse en milisegundos, no segundos. Miles de tests en segundos.
  2. Deterministas: mismos inputs → mismos outputs. Sin sorpresas por red, tiempo o aleatoriedad.
  3. Independientes: que un test pase o falle no debe depender del orden de ejecución ni del estado de otros tests.

El mocking reemplaza dependencias externas con sustitutos controlados. Tú defines exactamente qué retorna cada llamada. Los tests dejan de depender del mundo real y pasan a depender solo de tu lógica.

Mocking como sustituto controlado

Un mock es un objeto que simula el comportamiento de otro. En lugar de llamar a la API real, llamas a un objeto que retorna lo que tú configuraste. Así puedes:

  • Verificar que tu código llamó a la API con los parámetros correctos
  • Simular respuestas exitosas, errores, timeouts
  • Ejecutar cientos de tests sin una sola petición HTTP real

unittest.mock: Mock y MagicMock

Mock(): objeto mock genérico

Mock() crea un objeto que puede actuar como cualquier cosa. Cualquier atributo que accedas existe automáticamente. Cualquier método que llames retorna otro Mock por defecto.

from unittest.mock import Mock

# Crear un mock
mock_api = Mock()

# Configurar qué retorna un método cuando se llama
mock_api.get_user.return_value = {"id": 1, "name": "Alice"}

# Llamar al método
result = mock_api.get_user(1)

# Verificar el resultado
assert result["name"] == "Alice"

# Verificar que se llamó correctamente
mock_api.get_user.assert_called_once_with(1)

No importa qué argumentos pases — el mock retorna lo que configuraste con return_value. Puedes pasar get_user(999) o get_user() y seguirá retornando {"id": 1, "name": "Alice"}. Los assertions verifican cómo se usó el mock.

MagicMock(): Mock con métodos mágicos

MagicMock es como Mock pero con los métodos mágicos de Python preconfigurados: __len__, __iter__, __getitem__, etc. Úsalo cuando el código que estás testeando usa operadores o built-ins que dependen de estos métodos.

from unittest.mock import MagicMock

# MagicMock soporta len(), iteración, indexado
mock_list = MagicMock()
mock_list.__len__.return_value = 3
assert len(mock_list) == 3

mock_dict = MagicMock()
mock_dict.__getitem__.return_value = "valor"
assert mock_dict["clave"] == "valor"

En la mayoría de los casos, Mock basta. Usa MagicMock cuando veas errores como TypeError: object of type 'Mock' has no len().

Cuándo usar Mock vs MagicMock

SituaciónUsar
Objeto que simula una API, servicio o dependenciaMock
Objeto que se usa con len(), for x in obj, obj[key]MagicMock
Objeto que se compara con ==, in, operadoresMagicMock (define __eq__, __contains__, etc.)
No estás seguroMagicMock — es un superset, rara vez falla

MagicMock es más permisivo: responde a más operaciones sin configurar nada. El costo es que puede ocultar bugs si tu código usa un objeto de forma incorrecta y el mock "funciona" de todas formas. Para dependencias explícitas (métodos que llamas), Mock suele ser suficiente y más estricto.

return_value: qué retorna la llamada

return_value define el valor que el mock retorna cuando se invoca como función o método:

from unittest.mock import Mock

mock = Mock()
mock.calculate.return_value = 42

assert mock.calculate() == 42
assert mock.calculate(1, 2, 3) == 42  # Siempre 42, ignorando args

side_effect: excepciones, múltiples retornos, funciones custom

side_effect te da control fino sobre el comportamiento:

from unittest.mock import Mock

# Lanzar excepción
mock = Mock()
mock.risky_call.side_effect = ConnectionError("timeout")
# mock.risky_call() → lanza ConnectionError

# Retornar valores distintos en llamadas sucesivas
mock = Mock()
mock.fetch.side_effect = [{"id": 1}, {"id": 2}, {"id": 3}]
assert mock.fetch()["id"] == 1
assert mock.fetch()["id"] == 2
assert mock.fetch()["id"] == 3
# La 4ª llamada lanza StopIteration

# Función custom
mock = Mock()
mock.double.side_effect = lambda x: x * 2
assert mock.double(5) == 10

patch: reemplazar objetos reales temporalmente

Crear mocks manualmente y pasarlos como parámetros funciona cuando tu código acepta dependencias por inyección. Pero a menudo el código importa y usa el objeto directamente:

# mymodule.py
import requests

def fetch_data(url: str):
    response = requests.get(url)
    return response.json()

Aquí fetch_data usa requests.get directamente. Para mockearlo, necesitas reemplazar requests.get en el lugar donde se usa (en mymodule), no donde se define (en requests).

patch como decorador

# tests/test_mymodule.py
from unittest.mock import patch
from mymodule import fetch_data


@patch("mymodule.requests.get")
def test_fetch_data(mock_get):
    # mock_get reemplaza requests.get dentro de mymodule
    mock_get.return_value.json.return_value = {"data": "test"}

    result = fetch_data("https://api.example.com")

    assert result == {"data": "test"}
    mock_get.assert_called_once_with("https://api.example.com")

El string "mymodule.requests.get" es la ruta donde se usa el objeto. Si mymodule hace from requests import get y luego usa get, patcheas "mymodule.get". La regla: patch donde se USA, no donde se DEFINE.

Dónde patchear: regla de oro

La confusión más común con patch es el path. El objeto se busca en el namespace donde se usa, no donde se define. Ejemplos:

Import en mymoduleCódigo en mymodulePath correcto para patch
import requestsrequests.get(url)"mymodule.requests.get"
from requests import getget(url)"mymodule.get"
import requests as reqreq.get(url)"mymodule.req.get"
from openai import OpenAIclient = OpenAI(); client.chat..."mymodule.OpenAI" (patcheas la clase)

Si patcheas "requests.get" en vez de "mymodule.requests.get", el módulo mymodule ya tiene una referencia al requests.get original en su namespace. El patch en requests no afecta esa referencia. Por eso patcheas donde el nombre está vinculado en el módulo que hace la llamada.

patch como context manager

from unittest.mock import patch
from mymodule import fetch_data


def test_fetch_data_with_context():
    with patch("mymodule.requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"data": "test"}
        result = fetch_data("https://api.example.com")
    assert result == {"data": "test"}
    # Fuera del with, requests.get vuelve a ser el original

Útil cuando quieres aplicar el patch solo en parte del test.

patch con pytest-mock: la fixture mocker

pytest-mock provee la fixture mocker que simplifica el uso de patch:

# tests/test_mymodule.py
def test_fetch_data(mocker):
    mock_get = mocker.patch("mymodule.requests.get")
    mock_get.return_value.json.return_value = {"status": "ok"}

    from mymodule import fetch_data
    result = fetch_data("https://api.example.com")

    assert result == {"status": "ok"}
    mock_get.assert_called_once_with("https://api.example.com")

Con mocker, no necesitas decoradores ni context managers. La fixture se encarga del setup y teardown del patch. Instálalo con pip install pytest-mock.

Dónde patchear: la regla de oro

La causa más común de errores con patch es usar el path incorrecto. La regla es: patchea donde el objeto se USA, no donde se DEFINE.

# api_client.py
import requests

def fetch_users():
    return requests.get("https://api.example.com/users").json()

Aquí requests se importa en api_client, y se usa como requests.get. El path correcto es "api_client.requests.get" — el namespace donde se invoca.

Si el módulo hiciera esto:

# api_client.py
from requests import get

def fetch_users():
    return get("https://api.example.com/users").json()

Entonces get vive en el namespace de api_client. El path correcto es "api_client.get", no "requests.get".

Resumen: El string de patch debe ser la ruta completa al atributo tal como lo vería el código bajo test cuando se ejecuta. Si mymodule hace from foo import bar y usa bar(), patcheas "mymodule.bar".


Assertions sobre mocks

Los mocks registran cómo se usaron. Puedes verificar que tu código los invocó correctamente.

Métodos de verificación

from unittest.mock import Mock

mock = Mock()
mock.process(1, 2)
mock.process(3, 4)

# ¿Se llamó?
mock.process.assert_called()

# ¿Se llamó exactamente una vez?
mock.process.assert_called_once()

# ¿Se llamó con estos argumentos?
mock.process.assert_called_with(1, 2)

# ¿Se llamó exactamente una vez con estos argumentos?
mock.process.assert_called_once_with(1, 2)

# ¿No se llamó?
mock.other_method.assert_not_called()

Si el assertion falla, pytest muestra un mensaje claro: "Expected to be called once. Called 2 times."

Atributos para inspección

mock.process(1, 2)
mock.process(3, 4)

mock.process.call_count          # 2
mock.process.call_args           # call(3, 4) — última llamada
mock.process.call_args_list      # [call(1, 2), call(3, 4)]
mock.process.call_args[0]        # (3, 4) — args de la última llamada
mock.process.call_args[1]        # {} — kwargs de la última llamada

side_effect para escenarios complejos

Simular excepción

from unittest.mock import Mock

mock_api = Mock()
mock_api.fetch.side_effect = ConnectionError("Network unreachable")

# Tu código debe manejar la excepción
# El test verifica que tu código la maneje correctamente

Retornos diferentes en llamadas sucesivas

mock = Mock()
mock.get_next.side_effect = [1, 2, 3, StopIteration]

assert mock.get_next() == 1
assert mock.get_next() == 2
assert mock.get_next() == 3
# La 4ª llamada lanza StopIteration

Útil cuando tu código hace un loop que llama varias veces al mismo método.

Función custom por llamada

mock = Mock()
def custom_logic(user_id):
    if user_id == 1:
        return {"name": "Admin"}
    return {"name": "User"}

mock.get_user.side_effect = custom_logic

assert mock.get_user(1)["name"] == "Admin"
assert mock.get_user(2)["name"] == "User"

pytest-mock: la fixture mocker

pytest-mock añade la fixture mocker a pytest. Es un wrapper sobre unittest.mock con sintaxis más limpia.

Ventajas de mocker

  • No necesitas @patch como decorador
  • Los patches se limpian automáticamente después del test
  • Sintaxis más concisa
  • Compatible con el estilo de pytest (fixtures, parametrize)

Ejemplo completo

# src/ai_service.py
import openai

def generate_summary(text: str) -> str:
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": text}],
    )
    return response.choices[0].message["content"]
# tests/test_ai_service.py
def test_generate_summary(mocker):
    mock_create = mocker.patch("ai_service.openai.ChatCompletion.create")
    mock_create.return_value = {
        "choices": [{"message": {"content": "Short summary."}}]
    }

    from ai_service import generate_summary
    result = generate_summary("Long text to summarize.")

    assert result == "Short summary."
    mock_create.assert_called_once()
    call_kwargs = mock_create.call_args[1]
    assert call_kwargs["model"] == "gpt-4"
    assert "Long text" in call_kwargs["messages"][0]["content"]

Cuándo mockear y cuándo no

El sobre-mockeo es un error común. Mockear la función que estás testeando hace que el test no verifique nada.

Mockear

  • ✅ Llamadas a APIs HTTP (OpenAI, Stripe, etc.)
  • ✅ Consultas a base de datos
  • ✅ Lectura/escritura en filesystem
  • ✅ datetime.now(), time.sleep()
  • ✅ random.random(), uuid.uuid4()
  • ✅ Envío de emails, mensajes a colas

No mockear

  • ❌ La función que estás testeando
  • ❌ Lógica pura (cálculos, validaciones, transformaciones)
  • ❌ Estructuras de datos simples (listas, diccionarios)
  • ❌ Código que ya está aislado y es rápido

Casos dudosos (juicio)

  • ⚠️ Módulos internos del mismo proyecto: a veces mockear, a veces no — depende de si quieres un test de unidad (aislado) o de integración (con dependencias reales).
  • ⚠️ Servicios internos que son lentos: si un servicio interno tarda 5 segundos, mockear puede tener sentido en unit tests, pero en integration tests quizá quieras usar el real.

Heurística práctica: si es lento, no-determinista, costoso o tiene efectos secundarios externos, considéralo candidato a mockear.

Comparación: tests con mocks vs sin mocks

AspectoSin mocksCon mocks
VelocidadSegundos por test (red, DB, I/O)Milisegundos por test
DeterminismoDepende del estado externoControl total sobre inputs/outputs
Ejecución offlineRequiere servicios corriendoFunciona sin red ni DB
CosteAPIs consumen créditosCero coste
CI/CDFrágil si servicios fallanEstable e independiente
AislamientoUn fallo externo rompe muchos testsSolo falla la lógica que testeas

Claude Code y mocking

Pedir mocks explícitamente

Cuando pidas a Claude Code que genere tests para código con dependencias externas, especifica qué mockear y cómo. Un prompt genérico puede producir tests que llaman a la API real. Un prompt específico evita eso:

Genera unit tests para summarize_text() en summarizer.py.

Requisitos:
- Mockea httpx.post — no debe haber llamadas HTTP reales
- Usa patch o mocker.patch con path "summarizer.httpx.post"
- Verifica que summarize_text retorna el summary de la respuesta mockeada
- Incluye un test que verifica que texto vacío lanza ValueError
- Usa assert_called_once para verificar que se llamó a la API

Con ese contexto, Claude Code genera tests que usan mocks correctamente.

Evaluar tests generados

Cuando Claude Code te devuelve tests, verifica:

  • ¿El path de patch es correcto? (donde se usa, no donde se define)
  • ¿Se mockea la dependencia externa y no la función bajo test?
  • ¿Hay assertions que verifiquen cómo se usó el mock (assert_called_with, etc.)?
  • ¿Los tests pasan sin red, base de datos ni servicios externos?

Si un test tarda varios segundos o falla por "connection refused", probablemente está llamando al servicio real. Pide que mockee la dependencia.

Iterar cuando el mock no funciona

Si el test falla con "AttributeError" o el mock parece no aplicarse:

  1. Revisa el path: Dale a Claude Code el contenido del módulo y pídele: "¿Cuál es el path correcto para patchear la llamada a la API en este módulo?"
  2. Import dentro del test: Si el módulo se importa al inicio del archivo de tests, el patch puede aplicarse tarde. Pide que el import de la función bajo test esté dentro del test, después del patch.
  3. Dale el error exacto: "El test falla con: [pegar traceback]. Corrige el path de patch."

Práctica: mockeando un servicio de AI

Código completo ejecutable para que practiques.

Módulo bajo test

# src/summarizer.py
import httpx

def summarize_text(text: str, api_url: str = "https://api.example.com/summarize") -> str:
    """Envía texto a API externa y retorna el resumen."""
    if not text or not text.strip():
        raise ValueError("Text cannot be empty")
    response = httpx.post(api_url, json={"text": text}, timeout=10.0)
    response.raise_for_status()
    return response.json()["summary"]

Estructura del proyecto para practicar

project/
├── src/
│   └── summarizer.py
├── tests/
│   └── test_summarizer.py
└── pyproject.toml  # o requirements.txt con httpx

Asegúrate de tener httpx instalado (pip install httpx) y de que el directorio src esté en el path de Python. Si usas estructura estándar, ejecuta desde la raíz: pytest tests/ -v.

Tests con mocks

# tests/test_summarizer.py
import pytest
import httpx
from unittest.mock import Mock, patch


def test_summarize_returns_api_response():
    mock_response = Mock()
    mock_response.json.return_value = {"summary": "Short summary."}
    mock_response.raise_for_status = Mock()

    with patch("summarizer.httpx.post", return_value=mock_response) as mock_post:
        from summarizer import summarize_text
        result = summarize_text("Very long text that needs summarizing.")

    assert result == "Short summary."
    mock_post.assert_called_once()
    call_args = mock_post.call_args
    assert call_args[1]["json"]["text"] == "Very long text that needs summarizing."


def test_summarize_empty_text_raises():
    from summarizer import summarize_text

    with pytest.raises(ValueError, match="cannot be empty"):
        summarize_text("")


def test_summarize_http_error_propagates():
    """Cuando la API retorna error HTTP, httpx.raise_for_status() lanza."""
    mock_response = Mock()
    mock_response.status_code = 500
    mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
        "Server error", request=Mock(), response=mock_response
    )

    with patch("summarizer.httpx.post", return_value=mock_response):
        from summarizer import summarize_text

        with pytest.raises(httpx.HTTPStatusError):
            summarize_text("Valid text")

Ejercicios

Ejercicio 1: Mock básico con return_value (Fácil)

Crea un mock para un cliente de API que tiene el método get_user(id: int). Configura el mock para que retorne {"id": 42, "name": "Alice"}. Llama al método y verifica con assert que el nombre es "Alice". Usa assert_called_once_with(42) para verificar la llamada.

Ver solución
from unittest.mock import Mock

mock_client = Mock()
mock_client.get_user.return_value = {"id": 42, "name": "Alice"}

result = mock_client.get_user(42)

assert result["name"] == "Alice"
mock_client.get_user.assert_called_once_with(42)

Ejercicio 2: patch para mockear requests (Medio)

Tienes esta función que usa requests.get:

# weather.py
import requests

def get_temperature(city: str) -> float:
    r = requests.get(f"https://api.weather.com/{city}")
    return r.json()["temperature"]

Escribe un test que use @patch para mockear requests.get y verificar que get_temperature("madrid") retorna 22.5.

Ver solución
from unittest.mock import patch
from weather import get_temperature


@patch("weather.requests.get")
def test_get_temperature(mock_get):
    mock_get.return_value.json.return_value = {"temperature": 22.5}

    result = get_temperature("madrid")

    assert result == 22.5
    mock_get.assert_called_once_with("https://api.weather.com/madrid")

Nota: Se patchea "weather.requests.get" porque requests.get se usa dentro del módulo weather, no en el módulo de tests.

Ejercicio 3: side_effect para excepción (Medio)

Una función fetch_user(id: int) llama a api.get_user(id) y retorna el usuario. Si api.get_user lanza ConnectionError, la función debe retornar None. Escribe el test que verifica este comportamiento usando un mock con side_effect.

Ver solución
from unittest.mock import Mock, patch

# Asumiendo que user_service.py tiene:
# def fetch_user(id: int):
#     return api.get_user(id)  # retorna None si ConnectionError

@patch("user_service.api")
def test_fetch_user_connection_error_returns_none(mock_api):
    mock_api.get_user.side_effect = ConnectionError("timeout")

    from user_service import fetch_user
    result = fetch_user(1)

    assert result is None

Si fetch_user no captura la excepción aún, el test fallará hasta que implementes el manejo. Ese es el flujo TDD correcto.

Ejercicio 4: mocker fixture con pytest-mock (Medio)

Refactoriza el test del Ejercicio 2 para usar la fixture mocker de pytest-mock en lugar de @patch. Mantén la misma verificación.

Ver solución
def test_get_temperature(mocker):
    mock_get = mocker.patch("weather.requests.get")
    mock_get.return_value.json.return_value = {"temperature": 22.5}

    from weather import get_temperature
    result = get_temperature("madrid")

    assert result == 22.5
    mock_get.assert_called_once_with("https://api.weather.com/madrid")

Requiere pip install pytest-mock.

Ejercicio 5: side_effect con múltiples retornos (Medio)

Un mock de queue.pop() debe retornar "first", luego "second", y en la tercera llamada lanzar IndexError. Configura el mock y escribe un test que verifique los dos primeros retornos y que la tercera llamada lanza la excepción.

Ver solución
from unittest.mock import Mock
import pytest

mock_queue = Mock()
mock_queue.pop.side_effect = ["first", "second", IndexError("empty")]

assert mock_queue.pop() == "first"
assert mock_queue.pop() == "second"

with pytest.raises(IndexError, match="empty"):
    mock_queue.pop()

Nota: IndexError("empty") como elemento de la lista hace que la tercera llamada lance esa excepción. Si usas IndexError (la clase sin instanciar), side_effect la instanciará al llamar.

Ejercicio 6: Cuándo mockear — decisión (Fácil)

Para cada caso, indica si mockearías o no (y por qué):

  • a) Función calculate_discount(price, percent) que solo hace operaciones matemáticas
  • b) Función send_notification(user_id) que envía email vía SendGrid
  • c) Función parse_json(text) que usa json.loads de la stdlib
Ver solución
  • a) No mockear. Es lógica pura, rápida y determinista. Testear con valores reales.
  • b) Mockear. SendGrid es una API externa: lenta, con coste y efectos secundarios. Mockea la llamada a SendGrid y verifica que tu código la invoca con los parámetros correctos.
  • c) Generalmente no mockear. json.loads es de la stdlib, rápido y determinista. Mockearlo añade complejidad sin beneficio. Excepción: si quieres simular datos corruptos o malformados que hagan que json.loads lance — en ese caso side_effect con la excepción podría tener sentido, pero normalmente pasar inputs inválidos es suficiente.

Troubleshooting

Problema 1: "AttributeError: module 'X' has no attribute 'Y'" al usar patch

Causa: El path de patch es incorrecto. Patchas donde se define el objeto en vez de donde se usa.

Solución: Si en mymodule.py haces import requests y luego requests.get(url), el path es "mymodule.requests.get". Si haces from requests import get y luego get(url), el path es "mymodule.get". Siempre patchea en el namespace del módulo que hace la llamada.

Problema 2: El mock no se usa — el código sigue llamando al objeto real

Causa: Importas el módulo antes de aplicar el patch, o el patch está en el path equivocado.

Solución: Aplica el patch antes de que el módulo cargue el objeto. Con decoradores, el orden suele ser correcto. Con mocker.patch, asegúrate de que el patch esté activo antes del import que ejecuta el código. A veces necesitas hacer el import dentro del test, después del patch.

Problema 3: "assert_called_once_with" falla con "Expected call ... Actual call ..."

Causa: Los argumentos reales no coinciden con los esperados (orden, tipos, valores).

Solución: Revisa mock.call_args o mock.call_args_list para ver exactamente qué se pasó. Ajusta tu assert o corrige el código bajo test si los argumentos son incorrectos.

Problema 4: Mock retorna otro Mock en cadena (ej. mock.a.b.c) y el test falla

Causa: Solo configuraste return_value en el primer nivel. mock.a retorna un Mock, mock.a.b retorna otro Mock, etc. Si tu código espera un valor concreto en mock.a.b.c, necesitas configurarlo.

Solución: Configura la cadena explícitamente: mock.a.b.c.return_value = 42 o mock.a.return_value.b.c.return_value = 42. O configura cada nivel según la estructura que espera tu código.

Problema 5: side_effect con lista se agota y lanza StopIteration

Causa: Tu código hace más llamadas de las que configuraste en la lista.

Solución: Añade más elementos a la lista o usa una función en side_effect que maneje cualquier número de llamadas. Si el número de llamadas es parte de lo que quieres verificar, un assert call_count == N puede ayudar.


Conexión con Proyecto

El proyecto de este módulo es un Validation pipeline que depende de servicios externos: la API de OpenAI (u otra API de AI) y una base de datos. Usarás mocks para:

  • Reemplazar las llamadas a la API de AI con respuestas predefinidas
  • Reemplazar las consultas a la base de datos con un mock que retorna datos controlados

Los fundamentos de esta cápsula (Mock, patch, return_value, side_effect, assertions) son la base para ese proyecto. Sin dominar estos conceptos, no podrás aislar tu lógica de los servicios externos.

En la siguiente cápsula (Fixtures avanzadas) aprenderás a combinar mocks con fixtures de pytest para tener un setup de test limpio y reutilizable.


Resumen

  • Los tests deben ser rápidos, deterministas e independientes; el mocking reemplaza dependencias externas para lograrlo.
  • Mock() y MagicMock() crean objetos que simulan comportamiento; return_value fija el retorno, side_effect permite excepciones, múltiples retornos o funciones custom.
  • patch reemplaza objetos temporalmente; hay que patchear donde se usa el objeto, no donde se define.
  • Las assertions (assert_called_once_with, call_count, etc.) verifican que el código usó el mock correctamente.
  • pytest-mock y la fixture mocker simplifican el uso de patch en tests con pytest.
  • Mockea servicios externos (APIs, DB, filesystem, tiempo); no mockees la función bajo test ni lógica pura.

Próxima cápsula: Fixtures avanzadas de pytest — scope, autouse, factories y conftest.py.


Recursos Adicionales

  1. unittest.mock — Python Documentation — Referencia oficial de Mock, MagicMock y patch
  2. pytest-mock — Plugin de pytest para mocking
  3. Where to patch — Explicación detallada de patch paths
  4. Martin Fowler: Mocks Aren't Stubs — Diferencias entre mocks, stubs y fakes
  5. Real Python: Python Mocking — Tutorial práctico de unittest.mock
  6. Google Testing Blog: Testing on the Toilet — Buenas prácticas de testing (buscar artículos sobre mocking)

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