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.
Un mock es un doble de prueba que permite comprobar cómo una unidad interactúa con una dependencia.
Puede ayudarnos a verificar:
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.
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.
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.
Verificar interacción tiene sentido cuando la colaboración es el efecto importante.
Ejemplos:
Si el resultado puede verificarse directamente, quizá no hace falta usar un mock.
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.
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.
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.
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.
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.
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(). |
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.
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.
Los mocks son adecuados cuando:
Conviene evitar mocks cuando:
Un mock debe aportar información, no solo complejidad.
| Situación | ¿Mock útil? | Motivo |
|---|---|---|
| Enviar notificación al aprobar pedido. | Sí | La interacción es el efecto importante. |
| Calcular total de una lista. | No | El resultado se verifica directamente. |
| Registrar evento de auditoría. | Sí | 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. |
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.
Antes de usar un mock, revisa:
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.