Ya vimos muchas herramientas: objetos reales, stubs, fakes, mocks, spies, patch, monkeypatch y AsyncMock. La pregunta práctica es: ¿cuál conviene usar en cada caso?
En este tema construiremos una estrategia de decisión para combinar estas herramientas sin escribir pruebas frágiles ni pruebas demasiado lentas.
Usa el reemplazo más simple que permita verificar el comportamiento con claridad. No uses mocks si un dato real alcanza. No uses una base real si solo necesitas una respuesta controlada. No uses un stub si necesitas verificar una interacción externa crítica.
La herramienta debe servir a la intención de la prueba, no al revés.
Una guía práctica:
Supongamos un servicio de pedidos:
class ServicioPedidos:
def __init__(self, repositorio, pagos, email, auditoria):
self.repositorio = repositorio
self.pagos = pagos
self.email = email
self.auditoria = auditoria
def confirmar(self, pedido_id):
pedido = self.repositorio.buscar_por_id(pedido_id)
if pedido is None:
return False
resultado = self.pagos.cobrar(pedido["tarjeta"], pedido["total"])
if not resultado["aprobado"]:
self.auditoria.registrar("pago_rechazado", pedido_id)
return False
pedido["estado"] = "confirmado"
self.repositorio.guardar(pedido)
self.email.enviar_confirmacion(pedido["email"], pedido_id)
self.auditoria.registrar("pedido_confirmado", pedido_id)
return True
Este servicio combina estado, pago, email y auditoría. No todas las dependencias se prueban igual.
El repositorio conserva estado, así que un fake puede ser mejor que un mock:
class RepositorioPedidosFake:
def __init__(self, pedidos):
self.pedidos = {pedido["id"]: pedido.copy() for pedido in pedidos}
def buscar_por_id(self, pedido_id):
pedido = self.pedidos.get(pedido_id)
return None if pedido is None else pedido.copy()
def guardar(self, pedido):
self.pedidos[pedido["id"]] = pedido.copy()
Esto permite verificar estado final del pedido sin acoplarse a la llamada exacta a guardar.
La pasarela de pago es una colaboración externa importante. Un mock permite controlar la respuesta y verificar la llamada:
pagos = Mock()
pagos.cobrar.return_value = {"aprobado": True}
En una prueba de confirmación, tiene sentido verificar que se intentó cobrar la tarjeta correcta con el monto correcto.
El email también es un efecto externo. Si solo esperamos un envío puntual, un mock es suficiente:
email.enviar_confirmacion.assert_called_once_with(
"ana@example.com",
10,
)
Si queremos inspeccionar varios emails como datos, un spy manual puede ser más claro.
Ejemplo:
from unittest.mock import Mock
def test_confirmar_pedido_aprobado():
repositorio = RepositorioPedidosFake([
{
"id": 10,
"email": "ana@example.com",
"tarjeta": "4111111111111111",
"total": 2500,
"estado": "pendiente",
}
])
pagos = Mock()
pagos.cobrar.return_value = {"aprobado": True}
email = Mock()
auditoria = Mock()
servicio = ServicioPedidos(repositorio, pagos, email, auditoria)
resultado = servicio.confirmar(10)
assert resultado is True
assert repositorio.buscar_por_id(10)["estado"] == "confirmado"
pagos.cobrar.assert_called_once_with("4111111111111111", 2500)
email.enviar_confirmacion.assert_called_once_with("ana@example.com", 10)
auditoria.registrar.assert_called_once_with("pedido_confirmado", 10)
Cada doble cumple un rol distinto.
Cuando el pedido no existe, no deberían ocurrir efectos externos:
def test_confirmar_pedido_inexistente_no_cobra_ni_notifica():
repositorio = RepositorioPedidosFake([])
pagos = Mock()
email = Mock()
auditoria = Mock()
servicio = ServicioPedidos(repositorio, pagos, email, auditoria)
resultado = servicio.confirmar(99)
assert resultado is False
pagos.cobrar.assert_not_called()
email.enviar_confirmacion.assert_not_called()
auditoria.registrar.assert_not_called()
Los mocks sirven para verificar que no se produjo ningún efecto externo.
Para pago rechazado:
def test_confirmar_pedido_rechazado_audita_y_no_envia_email():
repositorio = RepositorioPedidosFake([
{
"id": 10,
"email": "ana@example.com",
"tarjeta": "4000000000000002",
"total": 2500,
"estado": "pendiente",
}
])
pagos = Mock()
pagos.cobrar.return_value = {"aprobado": False}
email = Mock()
auditoria = Mock()
servicio = ServicioPedidos(repositorio, pagos, email, auditoria)
resultado = servicio.confirmar(10)
assert resultado is False
assert repositorio.buscar_por_id(10)["estado"] == "pendiente"
email.enviar_confirmacion.assert_not_called()
auditoria.registrar.assert_called_once_with("pago_rechazado", 10)
La prueba mezcla fake para estado y mocks para efectos externos.
Además de estas pruebas unitarias, conviene tener algunas pruebas de integración:
Las pruebas unitarias con dobles no reemplazan todas las pruebas de integración.
Una matriz simple:
No hace falta probar el mismo comportamiento en todos los niveles. Por ejemplo, si una regla de descuento está bien cubierta con pruebas unitarias, una prueba de integración puede limitarse a comprobar que el flujo completo conecta las piezas.
Repetir todas las combinaciones en pruebas lentas suele aumentar mantenimiento sin mejorar mucho la confianza.
Las pruebas con stubs, fakes y mocks suelen ser rápidas y precisas. Las pruebas con componentes reales suelen dar más confianza sobre integración, pero son más lentas y requieren más preparación.
Una buena suite combina ambas: muchas pruebas rápidas para reglas y algunas pruebas integradas para contratos importantes.
Antes de dejar una prueba con mocks, revisa:
Al crear un módulo nuevo:
Este enfoque reduce la necesidad de parches complejos.
Para este servicio, decide qué doble usarías para cada dependencia:
class ServicioReservas:
def __init__(self, repositorio, pagos, email, reloj):
self.repositorio = repositorio
self.pagos = pagos
self.email = email
self.reloj = reloj
def reservar(self, datos):
reserva = {
"id": datos["id"],
"email": datos["email"],
"fecha": self.reloj.ahora(),
"estado": "pendiente",
}
self.repositorio.guardar(reserva)
self.pagos.autorizar(datos["tarjeta"], datos["total"])
self.email.enviar_confirmacion(datos["email"], reserva["id"])
return reserva
Escribe una prueba de reserva exitosa.
Una combinación razonable: fake para repositorio, mock para pagos y email, stub para reloj.
from datetime import datetime
from unittest.mock import Mock
class RepositorioReservasFake:
def __init__(self):
self.reservas = {}
def guardar(self, reserva):
self.reservas[reserva["id"]] = reserva.copy()
class RelojStub:
def ahora(self):
return datetime(2026, 5, 15, 10, 30, 0)
def test_reservar_exitosamente():
repositorio = RepositorioReservasFake()
pagos = Mock()
email = Mock()
servicio = ServicioReservas(repositorio, pagos, email, RelojStub())
datos = {
"id": 7,
"email": "ana@example.com",
"tarjeta": "4111111111111111",
"total": 2500,
}
reserva = servicio.reservar(datos)
assert reserva["estado"] == "pendiente"
assert repositorio.reservas[7]["email"] == "ana@example.com"
pagos.autorizar.assert_called_once_with("4111111111111111", 2500)
email.enviar_confirmacion.assert_called_once_with("ana@example.com", 7)
Una buena estrategia de testing combina herramientas. Usa objetos reales para datos y funciones puras, stubs para respuestas controladas, fakes para estado, mocks para efectos externos y pruebas de integración para verificar infraestructura real.
En el próximo tema construiremos un caso práctico integrador para probar un servicio Python con dependencias externas.