Las fixtures de pytest ofrecen datos y recursos sin repetir preparación. Mocks y monkeypatch controlan estado externo como entorno o HTTP. Con moderación producen pruebas deterministas; en exceso validan una simulación.
Este artículo amplía la guía de pytest en Python.
import pytest
@pytest.fixture
def repositorio(tmp_path):
archivo = tmp_path / "usuarios.json"
archivo.write_text("[]", encoding="utf-8")
yield archivo
def test_empieza_vacio(repositorio):
assert repositorio.read_text(encoding="utf-8") == "[]"
El código anterior a yield prepara y el posterior puede limpiar. Mantén scope function salvo que compartir un recurso costoso sea seguro.
def test_modo_produccion(monkeypatch):
monkeypatch.setenv("APP_MODE", "production")
assert modo_actual() == "production"
pytest restaura el entorno. La guía oficial de monkeypatch cubre atributos, diccionarios y rutas.
Para HTTP, sustituye el nombre usado por tu módulo y afirma el comportamiento de tu función. Conserva pruebas de integración para timeouts y el contrato real del cliente HTTP.
Componer fixtures pequeñas
Una fixture puede solicitar otra:
import json
@pytest.fixture
def almacen_vacio(tmp_path):
ruta = tmp_path / "usuarios.json"
ruta.write_text("[]", encoding="utf-8")
return ruta
@pytest.fixture
def almacen_lleno(almacen_vacio):
almacen_vacio.write_text(json.dumps([{"id": 1, "nombre": "Ada"}]), encoding="utf-8")
return almacen_vacio
Devuelve el valor cuando no hay limpieza. Usa yield si una conexión o transacción debe cerrarse. Si la preparación falla antes del yield, esa limpieza no se ejecuta; separa adquisiciones arriesgadas o utiliza un context manager.
Los scopes incluyen function, class, module, package y session. Un scope amplio ahorra tiempo, pero comparte estado. Una base mutable de sesión suele causar fallos dependientes del orden. Es más seguro compartir el engine y revertir una transacción por prueba.
Parametrizar variaciones
Cuando solo cambian entrada y resultado, usa parametrización:
@pytest.mark.parametrize(
("email", "valido"),
[("[email protected]", True), ("sin-arroba.example.com", False), ("", False)],
)
def test_validar_email(email, valido):
assert email_valido(email) is valido
Asigna id a casos complejos. Un bucle con muchas aserciones oculta casos posteriores al primer fallo. Consulta la guía oficial de parametrización.
Sustituir donde se consulta el nombre
Si app.cliente importa get mediante from requests import get, sustituye app.cliente.get, no requests.get. Los imports crean referencias locales. Mantén raising=True para detectar atributos mal escritos y usa setitem para mappings y setenv para entorno.
def test_ruta_config(monkeypatch, tmp_path):
with monkeypatch.context() as cambio:
cambio.setattr(Path, "home", lambda: tmp_path)
assert ruta_config() == tmp_path / ".miapp.toml"
pytest revierte el cambio incluso tras un fallo.
Usar Mock cuando la interacción importa
from unittest.mock import Mock
def test_notifica_despues_de_guardar():
repositorio = Mock()
notificador = Mock()
servicio = ServicioUsuario(repositorio, notificador)
servicio.crear("[email protected]")
repositorio.guardar.assert_called_once()
notificador.enviar.assert_called_once_with("[email protected]")
Usa spec o autospec para detectar atributos inexistentes. No compruebes cada llamada interna: el test se rompería con refactorizaciones inocuas. Un fake pequeño suele ser mejor para repositorios y colas. Para funciones asíncronas, utiliza AsyncMock; configura side_effect para errores o secuencias.
Controlar tiempo y azar
Inyecta una función en vez de parchear muchos detalles:
from datetime import datetime, timezone
def crear_registro(ahora=lambda: datetime.now(timezone.utc)):
return {"creado": ahora().isoformat()}
def test_registro_usa_hora_actual():
fija = datetime(2026, 1, 2, tzinfo=timezone.utc)
assert crear_registro(lambda: fija)["creado"] == "2026-01-02T00:00:00+00:00"
Aplica el patrón a UUID, azar y clientes de red.
Mantener conftest.py predecible
pytest descubre conftest.py según la jerarquía. La infraestructura global puede estar en la raíz; las fábricas específicas, junto a la funcionalidad. Las fixtures autouse merecen cautela porque modifican pruebas sin aparecer en sus parámetros. Son razonables para impedir red accidental, pero el estado oculto complica diagnósticos.
Una prueba unitaria rápida no exige simular todo. Funciones puras, directorios temporales, fakes y servidores locales ejercitan más comportamiento. Las pruebas de integración confirman serialización, restricciones de base, HTTP y configuración del framework. Una suite mantenible tiene preparación, acción y verificación claras, pruebas independientes y ningún orden obligatorio.
Las fixtures compartidas pueden vivir en conftest.py, pero deben permanecer cerca de sus consumidores. Usa tmp_path, prueba errores y evita red en pruebas unitarias. La referencia oficial enumera fixtures integradas.
Diagnosticar fallos sistemáticamente
Ejecuta primero la prueba mínima, luego el módulo y finalmente la suite. Si pasa sola pero falla en conjunto, busca estado global, recursos abiertos, fixtures reutilizadas o dependencia del orden. Revisa scope y limpieza antes de añadir reintentos. Repetir puede ayudar con un servicio externo, pero no debe ocultar unidades no deterministas.
Cuando falla una aserción de mock, mira los argumentos reales y pregunta si la expectativa representa un contrato público. Normalizar un correo antes de enviarlo puede ser intencional. Un mock aprobado tampoco demuestra que una API externa mantenga su firma; hace falta spec o integración.
Usa pytest -k para seleccionar escenarios y pytest -x cuando el primer fallo sea el más informativo. La salida detallada identifica parámetros y los logs explican fronteras. No dependas de variables locales ni archivos exclusivos del desarrollador.
Cubrir errores y límites
Para HTTP, prueba timeout, conexión rechazada, estado inesperado, JSON inválido y respuesta válida incompleta. Configura la simulación para cada frontera y confirma el comportamiento visible, no solo una llamada al mock.
Para base de datos, verifica restricciones, commit y rollback. Un repositorio fake prueba reglas del servicio, pero solo la base real confirma índices, tipos y aislamiento. Distribuye responsabilidades: unidad cubre decisiones; integración cubre adaptadores; punta a punta cubre pocos trayectos críticos.
Prueba valores límite como lista vacía, cero, fecha extrema y texto Unicode. No inventes casos solo para aumentar números. Cada escenario debe representar una regla o riesgo.
Crear datos legibles
Las factories pueden generar un objeto válido y sobrescribir el campo relevante. Evitan diccionarios enormes repetidos. Sin embargo, no ocultes campos obligatorios importantes detrás de valores mágicos.
Nombra situaciones como usuario_inactivo o pedido_pagado. El cuerpo debe dejar clara la diferencia que causa el resultado. Usa datos inventados, nunca información personal real.
Cuando una fixture persiste registros, devuelve el objeto creado. Limpia mediante transacciones cuando sea posible. Borrar manualmente por identificadores fijos es frágil y puede afectar otro escenario.
Revisar la calidad
La cobertura mide líneas ejecutadas, no la fuerza de las aserciones. Busca errores, límites y reglas cuya rotura tendría impacto. Cada prueba debe tener una razón principal para fallar. test_rechaza_email_duplicado informa más que test_usuario_2.
Elimina mocks obsoletos durante refactorizaciones. Si desaparece una colaboración, conservar su expectativa congela la arquitectura anterior. No cambies una prueba solo para verla verde: confirma si el comportamiento cambió deliberadamente.
Los comentarios explican decisiones inusuales, no repiten código. Preparación, acción y verificación visibles facilitan la revisión. Parametrizaciones complejas necesitan identificadores legibles.
Integrar la suite
Ejecuta pruebas rápidas con cada cambio y la suite antes de integrar. Marca claramente las externas y lentas, pero ejecútalas en el entorno adecuado. Una falla intermitente debe investigarse, no aceptarse.
Fija versiones cuando el proyecto necesita reproducibilidad y actualiza de forma controlada. Los warnings pueden anticipar roturas futuras y merecen atención.
Una buena suite funciona localmente y en integración continua porque controla tiempo, archivos, entorno y red. Es documentación ejecutable: explica lo que promete el sistema y permite refactorizar sin convertir cada detalle interno en contrato.
Finalmente, revisa que cada fixture tenga un consumidor claro y que su scope sea el menor posible. Una fixture no utilizada o demasiado amplia añade complejidad sin protección. Si su nombre no explica el recurso que entrega, renómbrala antes de multiplicar su uso.