29. Estrategia práctica para combinar pruebas reales, fakes, stubs y mocks

29.1 Objetivo del tema

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.

Objetivo práctico: elegir el tipo de prueba y el doble adecuado según el riesgo, la dependencia y el comportamiento que queremos verificar.

29.2 Principio general

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.

29.3 Orden de preferencia

Una guía práctica:

  • Usa objetos reales simples para datos y funciones puras.
  • Usa stubs para respuestas controladas.
  • Usa fakes cuando necesites estado en memoria.
  • Usa spies o mocks para verificar interacciones importantes.
  • Usa pruebas de integración para verificar infraestructura real.

29.4 Ejemplo de dominio

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.

29.5 Usar fake para repositorio

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.

29.6 Usar mock para pasarela de pago

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.

29.7 Usar mock o spy para email

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.

29.8 Prueba combinando herramientas

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.

29.9 Caso de pedido inexistente

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.

29.10 Caso de pago rechazado

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.

29.11 Dónde ubicar pruebas de integración

Además de estas pruebas unitarias, conviene tener algunas pruebas de integración:

  • Repositorio real contra una base de prueba.
  • Cliente HTTP contra un servidor falso o entorno controlado.
  • Serialización real de eventos enviados a una cola.
  • Configuración real cargada desde archivos o variables de entorno de prueba.

Las pruebas unitarias con dobles no reemplazan todas las pruebas de integración.

29.12 Matriz de decisión

Una matriz simple:

  • Cálculo puro: objetos reales y aserciones directas.
  • Consulta simple: stub.
  • Estado mutable: fake.
  • Efecto externo: mock o spy.
  • Infraestructura real: prueba de integración.
  • Código legacy difícil: patch temporal y luego refactorización gradual.

29.13 Evitar duplicación de escenarios

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.

29.14 Balance entre velocidad y 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.

29.15 Revisar una prueba antes de aceptarla

Antes de dejar una prueba con mocks, revisa:

  • ¿El mock representa una colaboración real?
  • ¿Estoy verificando comportamiento o implementación?
  • ¿Un stub o fake sería más claro?
  • ¿La prueba fallaría por una refactorización interna válida?
  • ¿Hace falta una prueba de integración complementaria?

29.16 Estrategia para un módulo nuevo

Al crear un módulo nuevo:

  • Diseña funciones puras para reglas y cálculos.
  • Inyecta dependencias externas.
  • Usa fakes para repositorios en memoria.
  • Usa mocks para pagos, emails, eventos y auditoría.
  • Agrega pruebas de integración para adaptadores reales.

Este enfoque reduce la necesidad de parches complejos.

29.17 Ejercicio práctico

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.

29.18 Solución posible del ejercicio

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)

29.19 Conclusión

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.