11. Verificar llamadas con assert_called, assert_called_once_with y call_args

11.1 Objetivo del tema

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.

Objetivo práctico: verificar interacciones relevantes sin convertir las pruebas en copias rígidas de la implementación.

11.2 Cuándo tiene sentido verificar llamadas

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.

11.3 Función de ejemplo

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.

11.4 assert_called

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.

11.5 assert_called_once

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.

11.6 assert_called_with

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.

11.7 assert_called_once_with

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.

11.8 assert_not_called

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.

11.9 Verificar guardado

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.

11.10 call_args

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.

11.11 call_count

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

11.12 call_args_list

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.

11.13 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.

11.14 Verificar kwargs

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",
    )

11.15 Usar ANY cuando un valor no importa

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.

11.16 Cuidado con verificar demasiado

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.

Verifica interacciones cuando sean parte del contrato observable, no cuando sean simples detalles de implementación.

11.17 Ejercicio práctico

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.

11.18 Solución posible del ejercicio

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.

11.19 Conclusión

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.