8. Introducción a unittest.mock: Mock y MagicMock

8.1 Objetivo del tema

Hasta ahora creamos stubs, spies y fakes manualmente. Python también incluye una herramienta estándar para crear dobles de prueba de forma rápida: unittest.mock.

En este tema veremos los objetos Mock y MagicMock, cómo se configuran, cómo registran llamadas y cuándo conviene usarlos.

Objetivo práctico: usar Mock y MagicMock para reemplazar dependencias sin escribir clases manuales en cada prueba.

8.2 Importar Mock

unittest.mock forma parte de la biblioteca estándar de Python, por lo que no hace falta instalarlo. Se importa así:

from unittest.mock import Mock

Podemos usarlo junto con pytest sin problema. pytest ejecuta la prueba, y Mock nos ayuda a construir el doble.

8.3 Primer Mock como stub

Volvamos al ejemplo de envío gratis:

def tiene_envio_gratis(cliente_id, servicio_compras):
    total = servicio_compras.obtener_total_comprado(cliente_id)
    return total >= 50000

Antes escribíamos una clase stub. Con Mock podemos crear un objeto y configurar el método que necesitamos:

from unittest.mock import Mock

from tienda.promociones import tiene_envio_gratis


def test_cliente_tiene_envio_gratis_con_mock():
    servicio = Mock()
    servicio.obtener_total_comprado.return_value = 65000

    resultado = tiene_envio_gratis("CLI-100", servicio)

    assert resultado is True

En esta prueba, el mock cumple el rol de stub porque devuelve un dato preparado.

8.4 Métodos creados dinámicamente

Un objeto Mock crea atributos y métodos de forma dinámica. Si escribimos servicio.obtener_total_comprado, Python no exige que el método exista de antemano.

Esto hace que Mock sea muy flexible, pero también puede ocultar errores de nombres mal escritos. Más adelante veremos spec y autospec para reducir ese riesgo.

Mock es cómodo porque acepta casi cualquier atributo. Esa flexibilidad debe usarse con cuidado.

8.5 Mock como spy

Además de devolver valores, un mock registra cómo fue usado. Esto permite verificar llamadas:

def registrar_pedido(pedido, repositorio, notificador):
    pedido_id = repositorio.guardar(pedido)
    notificador.enviar_confirmacion(pedido_id)
    return pedido_id

Podemos verificar que el notificador fue llamado:

from unittest.mock import Mock


def test_registrar_pedido_envia_confirmacion():
    repositorio = Mock()
    repositorio.guardar.return_value = 10
    notificador = Mock()

    pedido_id = registrar_pedido({"total": 500}, repositorio, notificador)

    assert pedido_id == 10
    notificador.enviar_confirmacion.assert_called_once_with(10)

En este caso, notificador funciona como un mock porque verificamos una interacción esperada.

8.6 Verificar si fue llamado

Los mocks tienen métodos de aserción integrados. Algunos de los más usados son:

  • assert_called(): verifica que fue llamado al menos una vez.
  • assert_called_once(): verifica que fue llamado exactamente una vez.
  • assert_called_with(...): verifica los argumentos de la última llamada.
  • assert_called_once_with(...): verifica una única llamada con argumentos específicos.
  • assert_not_called(): verifica que no fue llamado.

Más adelante dedicaremos un tema completo a verificar llamadas y argumentos.

8.7 Configurar un Mock al crearlo

También podemos pasar el valor de retorno al constructor:

servicio = Mock()
servicio.obtener_total_comprado = Mock(return_value=65000)

O crear directamente un mock que represente una función:

generar_token = Mock(return_value="ABC123")

codigo = generar_token()

assert codigo == "ABC123"
generar_token.assert_called_once()

Esto es útil cuando la dependencia inyectada es una función y no un objeto con métodos.

8.8 Mock como función inyectada

Supongamos esta función:

def crear_codigo_recuperacion(usuario_id, generar_token):
    token = generar_token()
    return f"{usuario_id}-{token}"

Podemos probarla con un mock que simula la función generadora:

def test_crear_codigo_recuperacion():
    generar_token = Mock(return_value="XYZ999")

    codigo = crear_codigo_recuperacion("USR-1", generar_token)

    assert codigo == "USR-1-XYZ999"
    generar_token.assert_called_once_with()

La prueba controla el token y verifica que la función fue invocada.

8.9 Mock con atributos

Un mock también puede simular objetos con atributos simples:

usuario = Mock()
usuario.id = 1
usuario.nombre = "Ana"
usuario.activo = True

Esto puede ser útil si el código bajo prueba espera acceder a propiedades:

def obtener_nombre_visible(usuario):
    if not usuario.activo:
        return "Usuario inactivo"
    return usuario.nombre

La prueba:

def test_obtener_nombre_visible_para_usuario_activo():
    usuario = Mock()
    usuario.nombre = "Ana"
    usuario.activo = True

    assert obtener_nombre_visible(usuario) == "Ana"

8.10 Cuándo preferir un objeto real simple

Aunque Mock puede simular atributos, a veces es más claro usar un diccionario, una dataclass o una clase pequeña real.

from dataclasses import dataclass


@dataclass
class Usuario:
    nombre: str
    activo: bool

Y la prueba:

def test_obtener_nombre_visible_con_objeto_real_simple():
    usuario = Usuario(nombre="Ana", activo=True)

    assert obtener_nombre_visible(usuario) == "Ana"

Si el objeto es solo dato, un objeto real suele ser más expresivo que un mock.

8.11 MagicMock

MagicMock es una variante de Mock que ya incluye soporte para muchos métodos mágicos de Python, como __len__, __iter__, __enter__, __exit__ y otros.

Se importa así:

from unittest.mock import MagicMock

Es útil cuando la dependencia se usa como colección, context manager, iterable u objeto con comportamiento especial.

8.12 Ejemplo simple con MagicMock

Supongamos una función que usa len sobre un objeto recibido:

def tiene_elementos(coleccion):
    return len(coleccion) > 0

Con MagicMock podemos configurar __len__:

def test_tiene_elementos_con_magicmock():
    coleccion = MagicMock()
    coleccion.__len__.return_value = 3

    assert tiene_elementos(coleccion) is True

Un Mock común no está pensado para cubrir tan cómodamente este tipo de métodos especiales.

8.13 MagicMock como context manager

Más adelante veremos ejemplos con archivos y context managers. Por ahora observa la idea:

recurso = MagicMock()
recurso.__enter__.return_value = "contenido"
recurso.__exit__.return_value = None

with recurso as valor:
    assert valor == "contenido"

recurso.__enter__.assert_called_once()
recurso.__exit__.assert_called_once()

MagicMock facilita probar código que usa la sentencia with.

8.14 Mock no reemplaza el criterio

Usar Mock puede reducir código repetido, pero también puede hacer que la prueba sea demasiado artificial si mockeamos todo.

Si un stub manual deja más claro el escenario, úsalo. Si un fake en memoria expresa mejor el comportamiento, úsalo. Mock es una herramienta, no una obligación.

La mejor prueba no es la que usa más mocks, sino la que comunica mejor el comportamiento esperado.

8.15 Ejercicio práctico

Prueba esta función usando Mock:

def enviar_codigo_login(usuario, generar_codigo, servicio_sms):
    codigo = generar_codigo()
    servicio_sms.enviar(usuario["telefono"], f"Tu código es {codigo}")
    return codigo

La prueba debe verificar que se devuelve el código generado y que el servicio SMS fue llamado con el teléfono y el mensaje correcto.

8.16 Solución posible del ejercicio

Una solución con Mock:

from unittest.mock import Mock

from seguridad import enviar_codigo_login


def test_enviar_codigo_login():
    usuario = {"telefono": "+5493511111111"}
    generar_codigo = Mock(return_value="123456")
    servicio_sms = Mock()

    codigo = enviar_codigo_login(usuario, generar_codigo, servicio_sms)

    assert codigo == "123456"
    generar_codigo.assert_called_once_with()
    servicio_sms.enviar.assert_called_once_with(
        "+5493511111111",
        "Tu código es 123456",
    )

El mock de generar_codigo cumple el rol de stub y spy. El mock de servicio_sms permite verificar la interacción.

8.17 Conclusión

Mock permite crear dobles de prueba rápidamente, configurar valores de retorno y verificar llamadas. MagicMock agrega soporte cómodo para métodos mágicos y objetos usados con comportamientos especiales.

En el próximo tema profundizaremos en return_value, una de las formas más comunes de configurar respuestas en mocks.