28. Mocks: verificar interacciones básicas

28.1 Introducción

En el tema anterior vimos stubs, que devuelven respuestas controladas. Ahora veremos mocks, dobles de prueba usados para verificar interacciones.

Un mock ayuda a responder preguntas como: ¿la unidad llamó a esta dependencia?, ¿la llamó con los datos correctos?, ¿la llamó la cantidad esperada de veces?

Los mocks son útiles, pero deben usarse con cuidado. Si verificamos demasiados detalles internos, las pruebas se vuelven frágiles.

28.2 Qué es un mock

Un mock es un doble de prueba que permite comprobar cómo una unidad interactúa con una dependencia.

Puede ayudarnos a verificar:

  • Si se llamó a un método.
  • Con qué argumentos se llamó.
  • Cuántas veces se llamó.
  • Si no se llamó cuando no correspondía.
Un mock se enfoca en la interacción. No pregunta solo qué resultado salió, sino qué colaboración ocurrió.

28.3 Resultado contra interacción

Muchas pruebas verifican resultados:

def test_calcular_total():
    assert calcular_total([100, 200]) == 300

Una prueba con mock verifica una colaboración:

def test_pedido_aprobado_envia_notificacion():
    # Verifica que se llame al notificador con cierto mensaje
    ...

Usamos mocks cuando el comportamiento importante es una interacción con otra dependencia.

28.4 Ejemplo sin biblioteca de mocks

Podemos entender el concepto creando un objeto que registre llamadas. A veces esto se llama spy, pero sirve para explicar la idea de verificar interacción.

class NotificadorMock:
    def __init__(self):
        self.mensajes_enviados = []

    def enviar(self, mensaje):
        self.mensajes_enviados.append(mensaje)


def aprobar_pedido(pedido, notificador):
    pedido["estado"] = "aprobado"
    notificador.enviar("Pedido aprobado")

El doble registra los mensajes enviados para que la prueba pueda verificarlos.

28.5 Verificar que se llamó a la dependencia

def test_aprobar_pedido_envia_notificacion():
    pedido = {"estado": "pendiente"}
    notificador = NotificadorMock()

    aprobar_pedido(pedido, notificador)

    assert notificador.mensajes_enviados == ["Pedido aprobado"]

La prueba verifica que la unidad interactuó con el notificador. También podría verificar el cambio de estado si ese comportamiento forma parte del mismo caso.

28.6 Cuándo tiene sentido verificar interacción

Verificar interacción tiene sentido cuando la colaboración es el efecto importante.

Ejemplos:

  • Enviar una notificación.
  • Registrar un evento de auditoría.
  • Guardar una entidad usando un repositorio.
  • Publicar un mensaje en una cola.
  • Llamar a una dependencia con datos transformados.

Si el resultado puede verificarse directamente, quizá no hace falta usar un mock.

28.7 Ejemplo con auditoría

Supongamos que al cambiar el estado de un usuario se debe registrar un evento.

class AuditoriaMock:
    def __init__(self):
        self.eventos = []

    def registrar(self, evento):
        self.eventos.append(evento)


def activar_usuario(usuario, auditoria):
    usuario["activo"] = True
    auditoria.registrar("usuario_activado")

La auditoría es una interacción importante. Podemos verificarla.

28.8 Prueba con auditoría

def test_activar_usuario_registra_evento_de_auditoria():
    usuario = {"activo": False}
    auditoria = AuditoriaMock()

    activar_usuario(usuario, auditoria)

    assert auditoria.eventos == ["usuario_activado"]

La prueba verifica que la interacción esperada ocurrió con el evento correcto.

28.9 Verificar que no se llama

A veces el comportamiento importante es que una dependencia no sea llamada.

def aprobar_pedido_si_tiene_total(pedido, notificador):
    if pedido["total"] <= 0:
        return False
    pedido["estado"] = "aprobado"
    notificador.enviar("Pedido aprobado")
    return True


def test_pedido_con_total_cero_no_envia_notificacion():
    pedido = {"total": 0, "estado": "pendiente"}
    notificador = NotificadorMock()

    resultado = aprobar_pedido_si_tiene_total(pedido, notificador)

    assert resultado == False
    assert notificador.mensajes_enviados == []

La prueba confirma que no se notificó un pedido inválido.

28.10 Mocks con bibliotecas

Los lenguajes y frameworks suelen ofrecer bibliotecas para crear mocks. En Python, por ejemplo, existe unittest.mock.

from unittest.mock import Mock


def test_aprobar_pedido_envia_notificacion():
    pedido = {"estado": "pendiente"}
    notificador = Mock()

    aprobar_pedido(pedido, notificador)

    notificador.enviar.assert_called_once_with("Pedido aprobado")

La biblioteca permite verificar la llamada sin escribir una clase manual.

28.11 Verificar argumentos

Una de las ventajas de los mocks es comprobar los argumentos usados en la interacción.

def registrar_compra(compra, auditoria):
    auditoria.registrar({
        "tipo": "compra",
        "total": compra["total"]
    })


def test_registrar_compra_envia_total_a_auditoria():
    auditoria = Mock()
    compra = {"total": 500}

    registrar_compra(compra, auditoria)

    auditoria.registrar.assert_called_once_with({
        "tipo": "compra",
        "total": 500
    })

La prueba verifica que la unidad envió los datos esperados a la dependencia.

28.12 Diferencia entre mock y stub

Un stub devuelve datos. Un mock verifica llamadas. A veces un mismo objeto puede cumplir ambos roles, pero la intención de la prueba debe estar clara.

Tipo Uso principal Ejemplo
Stub Controlar una respuesta. Devolver cotización 250.
Mock Verificar una interacción. Confirmar que se llamó a enviar().

28.13 Riesgo: probar detalles internos

Un riesgo de los mocks es terminar verificando pasos internos que no forman parte del comportamiento importante.

Ejemplo riesgoso:

def test_calcular_total_llama_a_sumar():
    # Verifica que internamente se use una función auxiliar llamada sumar
    ...

Si el resultado final es correcto, quizá no importa si la unidad usó una función auxiliar, un bucle o una fórmula directa. Verificar detalles internos vuelve la prueba frágil ante refactors.

28.14 Preferir resultados cuando sea posible

Si podemos verificar el resultado observable directamente, suele ser mejor que verificar una interacción interna.

def test_calcular_total():
    assert calcular_total([100, 200]) == 300

No hace falta mockear la suma ni verificar cada paso. El comportamiento importante es el total devuelto.

28.15 Cuándo usar mocks

Los mocks son adecuados cuando:

  • La interacción es el efecto observable principal.
  • No hay un valor de retorno suficiente para verificar el comportamiento.
  • Queremos evitar un efecto externo real.
  • Necesitamos confirmar que se envían datos correctos a una dependencia.
  • La dependencia representa una frontera del sistema.

28.16 Cuándo evitar mocks

Conviene evitar mocks cuando:

  • El resultado puede verificarse directamente.
  • La dependencia es un objeto simple y barato de usar.
  • El mock hace que la prueba conozca demasiados pasos internos.
  • La prueba falla ante refactors que no cambian comportamiento.
  • La preparación del mock es más compleja que el caso probado.

Un mock debe aportar información, no solo complejidad.

28.17 Tabla de ejemplos

Situación ¿Mock útil? Motivo
Enviar notificación al aprobar pedido. La interacción es el efecto importante.
Calcular total de una lista. No El resultado se verifica directamente.
Registrar evento de auditoría. Importa que se registre el evento correcto.
Validar longitud de contraseña. No No hay dependencia externa ni interacción relevante.
Confirmar guardado en repositorio real. Depende Mock para unidad; integración para repositorio real.

28.18 Mocks y diseño

Para usar mocks, la unidad debe recibir sus dependencias de forma reemplazable. Si la unidad crea internamente el servicio real, será más difícil sustituirlo en la prueba.

def procesar_pedido(pedido, notificador):
    pedido["estado"] = "aprobado"
    notificador.enviar("Pedido aprobado")

Como el notificador se recibe por parámetro, la prueba puede pasar un mock o spy controlado.

28.19 Lista de comprobación

Antes de usar un mock, revisa:

  • ¿La interacción es realmente importante?
  • ¿No hay un resultado más directo para verificar?
  • ¿El mock evita un efecto externo real?
  • ¿La prueba no queda acoplada a detalles internos?
  • ¿Los argumentos verificados forman parte del comportamiento esperado?
  • ¿La preparación del mock es razonable?
  • ¿Hace falta una prueba de integración complementaria?

28.20 Qué debes recordar de este tema

  • Un mock se usa para verificar interacciones.
  • Puede comprobar llamadas, argumentos y cantidad de llamadas.
  • Los mocks son útiles cuando la interacción es el efecto observable importante.
  • Si el resultado puede verificarse directamente, quizá no hace falta mock.
  • Abusar de mocks vuelve las pruebas frágiles.
  • Un mock no reemplaza pruebas de integración cuando la colaboración real importa.
  • La prueba debe seguir enfocada en comportamiento, no en detalles internos.

28.21 Conclusión

Los mocks permiten verificar interacciones básicas con dependencias. Son útiles cuando una unidad debe notificar, registrar, guardar o comunicarse con otro componente y esa colaboración es parte del comportamiento esperado.

La clave es usarlos con criterio. Un buen mock verifica una interacción relevante; un mal uso de mocks termina probando detalles internos y dificultando el refactoring.

En el próximo tema veremos fakes y objetos simulados simples, otra forma de reemplazar dependencias en pruebas unitarias.