Un mock no solo puede devolver valores. También registra cómo fue usado: cuántas veces se llamó, con qué argumentos y en qué orden. Esa información permite verificar interacciones importantes.
En este tema aprenderemos a usar assert_called, assert_called_once_with, call_args y otras herramientas relacionadas.
Verificar una llamada tiene sentido cuando la interacción es parte del comportamiento esperado. Por ejemplo, enviar una notificación, registrar una auditoría, publicar un evento o guardar un cambio.
No conviene verificar cada llamada interna solo porque el mock permite hacerlo. Si verificamos demasiados detalles, la prueba se rompe ante refactorizaciones que no cambian el comportamiento visible.
Usaremos esta función:
def activar_usuario(usuario_id, repositorio, email, auditoria):
usuario = repositorio.buscar_por_id(usuario_id)
if usuario is None:
return False
usuario["activo"] = True
repositorio.guardar(usuario)
email.enviar_bienvenida(usuario["email"])
auditoria.registrar("usuario_activado", usuario_id)
return True
La función modifica un usuario, lo guarda, envía un email y registra una auditoría. Algunas de esas interacciones pueden ser importantes para la prueba.
assert_called verifica que un mock fue llamado al menos una vez:
from unittest.mock import Mock
def test_activar_usuario_envia_email():
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"email": "ana@example.com",
"activo": False,
}
email = Mock()
auditoria = Mock()
activar_usuario(1, repositorio, email, auditoria)
email.enviar_bienvenida.assert_called()
Esta aserción confirma que hubo una llamada, pero no verifica argumentos ni cantidad exacta.
assert_called_once verifica que el mock fue llamado exactamente una vez:
def test_activar_usuario_envia_un_solo_email():
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"email": "ana@example.com",
"activo": False,
}
email = Mock()
auditoria = Mock()
activar_usuario(1, repositorio, email, auditoria)
email.enviar_bienvenida.assert_called_once()
Esto es más estricto. Úsalo cuando sea importante evitar llamadas duplicadas.
assert_called_with verifica los argumentos de la última llamada:
def test_activar_usuario_envia_email_al_destino_correcto():
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"email": "ana@example.com",
"activo": False,
}
email = Mock()
auditoria = Mock()
activar_usuario(1, repositorio, email, auditoria)
email.enviar_bienvenida.assert_called_with("ana@example.com")
Si el método fue llamado varias veces, esta aserción revisa la última llamada, no todas.
assert_called_once_with combina dos verificaciones: que se llamó una sola vez y que los argumentos coinciden.
def test_activar_usuario_envia_un_email_correcto():
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"email": "ana@example.com",
"activo": False,
}
email = Mock()
auditoria = Mock()
activar_usuario(1, repositorio, email, auditoria)
email.enviar_bienvenida.assert_called_once_with("ana@example.com")
Esta suele ser una de las aserciones más usadas cuando queremos verificar una interacción específica.
assert_not_called verifica que una interacción no ocurrió. Es útil para casos donde el código debe detenerse antes de llamar una dependencia.
def test_si_usuario_no_existe_no_envia_email():
repositorio = Mock()
repositorio.buscar_por_id.return_value = None
email = Mock()
auditoria = Mock()
resultado = activar_usuario(99, repositorio, email, auditoria)
assert resultado is False
email.enviar_bienvenida.assert_not_called()
auditoria.registrar.assert_not_called()
Esta prueba expresa una regla importante: no se notifica ni se audita una activación inexistente.
También podemos verificar que el repositorio recibió el usuario modificado:
def test_activar_usuario_guarda_usuario_activo():
repositorio = Mock()
usuario = {
"id": 1,
"email": "ana@example.com",
"activo": False,
}
repositorio.buscar_por_id.return_value = usuario
email = Mock()
auditoria = Mock()
activar_usuario(1, repositorio, email, auditoria)
repositorio.guardar.assert_called_once_with({
"id": 1,
"email": "ana@example.com",
"activo": True,
})
Como el diccionario fue modificado, la aserción comprueba el estado enviado al repositorio.
call_args permite inspeccionar los argumentos de la última llamada con más flexibilidad:
args, kwargs = repositorio.guardar.call_args
Ejemplo:
def test_activar_usuario_guarda_un_usuario_activo_usando_call_args():
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"email": "ana@example.com",
"activo": False,
}
email = Mock()
auditoria = Mock()
activar_usuario(1, repositorio, email, auditoria)
args, kwargs = repositorio.guardar.call_args
usuario_guardado = args[0]
assert kwargs == {}
assert usuario_guardado["activo"] is True
assert usuario_guardado["email"] == "ana@example.com"
Esto es útil cuando no queremos comparar todo el objeto, sino solo algunos campos relevantes.
call_count indica cuántas veces se llamó un mock:
assert email.enviar_bienvenida.call_count == 1
Puede ser útil en ciclos o reintentos:
def enviar_recordatorios(usuarios, email):
for usuario in usuarios:
email.enviar(usuario["email"], "Recordatorio")
def test_enviar_recordatorios_envia_un_email_por_usuario():
email = Mock()
usuarios = [
{"email": "ana@example.com"},
{"email": "luis@example.com"},
]
enviar_recordatorios(usuarios, email)
assert email.enviar.call_count == 2
call_args_list guarda todas las llamadas realizadas a un mock:
def test_enviar_recordatorios_verifica_destinos():
email = Mock()
usuarios = [
{"email": "ana@example.com"},
{"email": "luis@example.com"},
]
enviar_recordatorios(usuarios, email)
assert email.enviar.call_args_list[0].args == (
"ana@example.com",
"Recordatorio",
)
assert email.enviar.call_args_list[1].args == (
"luis@example.com",
"Recordatorio",
)
Esta herramienta es útil cuando importan varias llamadas, aunque en muchos casos hay una forma más clara con assert_has_calls.
Para verificar una secuencia de llamadas, usamos call y assert_has_calls:
from unittest.mock import call
def test_enviar_recordatorios_verifica_llamadas():
email = Mock()
usuarios = [
{"email": "ana@example.com"},
{"email": "luis@example.com"},
]
enviar_recordatorios(usuarios, email)
email.enviar.assert_has_calls([
call("ana@example.com", "Recordatorio"),
call("luis@example.com", "Recordatorio"),
])
Por defecto, se verifica que esas llamadas existan en ese orden relativo.
Si el código llama con argumentos nombrados, la verificación debe respetarlo:
def notificar_pago(email, pago):
email.enviar(
destino=pago["email"],
asunto="Pago recibido",
cuerpo=f"Recibimos tu pago de {pago['total']}",
)
Prueba:
def test_notificar_pago():
email = Mock()
notificar_pago(email, {
"email": "ana@example.com",
"total": 1500,
})
email.enviar.assert_called_once_with(
destino="ana@example.com",
asunto="Pago recibido",
cuerpo="Recibimos tu pago de 1500",
)
A veces un argumento cambia en cada ejecución, como una fecha o un identificador. Si ese valor no es el foco de la prueba, podemos usar ANY:
from unittest.mock import ANY
def test_registrar_auditoria_con_identificador_generado():
auditoria = Mock()
auditoria.registrar(evento="login", id_evento="ABC123")
auditoria.registrar.assert_called_once_with(
evento="login",
id_evento=ANY,
)
ANY indica que aceptamos cualquier valor para ese argumento.
Si una prueba verifica todas las llamadas internas, queda fuertemente acoplada a la implementación. Por ejemplo, no siempre hace falta comprobar que se llamó a cada método auxiliar si el resultado final ya demuestra el comportamiento.
Prueba esta función:
def procesar_pago(pago, pasarela, email, auditoria):
resultado = pasarela.cobrar(pago["tarjeta"], pago["total"])
if not resultado["aprobado"]:
auditoria.registrar("pago_rechazado", pago["id"])
return False
email.enviar_recibo(pago["email"], pago["total"])
auditoria.registrar("pago_aprobado", pago["id"])
return True
Escribe una prueba para pago aprobado y otra para pago rechazado. Verifica las llamadas importantes.
Una solución:
from unittest.mock import Mock
from pagos import procesar_pago
def test_procesar_pago_aprobado_envia_recibo_y_audita():
pasarela = Mock()
pasarela.cobrar.return_value = {"aprobado": True}
email = Mock()
auditoria = Mock()
pago = {
"id": 10,
"tarjeta": "4111111111111111",
"total": 2500,
"email": "ana@example.com",
}
resultado = procesar_pago(pago, pasarela, email, auditoria)
assert resultado is True
pasarela.cobrar.assert_called_once_with("4111111111111111", 2500)
email.enviar_recibo.assert_called_once_with("ana@example.com", 2500)
auditoria.registrar.assert_called_once_with("pago_aprobado", 10)
def test_procesar_pago_rechazado_no_envia_recibo_y_audita():
pasarela = Mock()
pasarela.cobrar.return_value = {"aprobado": False}
email = Mock()
auditoria = Mock()
pago = {
"id": 11,
"tarjeta": "4000000000000002",
"total": 900,
"email": "luis@example.com",
}
resultado = procesar_pago(pago, pasarela, email, auditoria)
assert resultado is False
email.enviar_recibo.assert_not_called()
auditoria.registrar.assert_called_once_with("pago_rechazado", 11)
Las verificaciones se concentran en las interacciones que expresan el comportamiento del flujo de pago.
Los mocks registran llamadas y permiten verificar interacciones con métodos como assert_called, assert_called_once_with, assert_not_called, call_args y assert_has_calls.
En el próximo tema veremos cómo probar colaboraciones sin acoplar las pruebas a detalles internos innecesarios.