17. Usar autospec y spec_set para detectar interfaces incorrectas

17.1 Objetivo del tema

Un Mock común es muy flexible: acepta atributos y métodos que no existen en el objeto real. Esa comodidad puede ocultar errores de nombres, firmas incorrectas y llamadas que fallarían en producción.

En este tema veremos spec, spec_set, create_autospec y patch(..., autospec=True) para construir mocks más seguros.

Objetivo práctico: evitar que los mocks acepten interfaces inventadas o llamadas con argumentos incorrectos.

17.2 El problema de Mock sin especificación

Supongamos esta clase real:

class ServicioEmail:
    def enviar_bienvenida(self, email):
        return True

Un mock común permite escribir cualquier método, incluso con errores de tipeo:

from unittest.mock import Mock


email = Mock()
email.enviar_bienvendia.return_value = True

El método enviar_bienvendia está mal escrito, pero Mock lo acepta.

17.3 Usar spec

spec limita los atributos disponibles en el mock según un objeto o clase real:

email = Mock(spec=ServicioEmail)

Ahora, si intentamos acceder a un método que no existe, Python lanzará AttributeError:

email.enviar_bienvenida.return_value = True

# Esto falla porque el nombre no existe en ServicioEmail:
email.enviar_bienvendia.return_value = True

spec ayuda a detectar errores de nombres.

17.4 Ejemplo con spec en una prueba

Función bajo prueba:

def registrar_usuario(usuario, servicio_email):
    servicio_email.enviar_bienvenida(usuario["email"])
    return True

Prueba:

from unittest.mock import Mock


def test_registrar_usuario_envia_bienvenida():
    servicio_email = Mock(spec=ServicioEmail)
    usuario = {"email": "ana@example.com"}

    resultado = registrar_usuario(usuario, servicio_email)

    assert resultado is True
    servicio_email.enviar_bienvenida.assert_called_once_with("ana@example.com")

El mock solo permite métodos presentes en ServicioEmail.

17.5 Límite de spec

spec evita acceder a atributos inexistentes, pero permite asignar atributos nuevos:

email = Mock(spec=ServicioEmail)
email.otro_atributo = "valor"

Si queremos impedir también la creación de atributos nuevos, usamos spec_set.

17.6 Usar spec_set

spec_set es más estricto. No permite acceder ni asignar atributos que no existan en la especificación:

email = Mock(spec_set=ServicioEmail)

email.enviar_bienvenida.return_value = True

# Esto falla:
email.otro_atributo = "valor"

Es útil cuando queremos que el mock se mantenga muy cerca de la interfaz real.

17.7 spec_set en una prueba

Ejemplo:

def test_registrar_usuario_con_spec_set():
    servicio_email = Mock(spec_set=ServicioEmail)
    usuario = {"email": "ana@example.com"}

    registrar_usuario(usuario, servicio_email)

    servicio_email.enviar_bienvenida.assert_called_once_with("ana@example.com")

Si la prueba o el código intentan usar un método que no existe, fallará más temprano.

17.8 El problema de la firma

spec y spec_set ayudan con nombres, pero no siempre controlan de forma estricta los argumentos al configurar métodos hijos. Para validar firmas de funciones y métodos, usamos autospec.

Supongamos:

def enviar_notificacion(email, asunto):
    return True

Queremos que el mock detecte si se llama con demasiados o muy pocos argumentos.

17.9 create_autospec para funciones

create_autospec crea un mock que respeta la firma de una función o clase:

from unittest.mock import create_autospec


enviar_mock = create_autospec(enviar_notificacion, return_value=True)

enviar_mock("ana@example.com", "Bienvenida")

# Esto falla por cantidad incorrecta de argumentos:
enviar_mock("ana@example.com")

Así detectamos errores que un Mock común aceptaría.

17.10 create_autospec para clases

También podemos crear un mock basado en una clase:

class RepositorioUsuarios:
    def buscar_por_id(self, usuario_id):
        pass

    def guardar(self, usuario):
        pass

Mock con autospec:

repositorio = create_autospec(RepositorioUsuarios, instance=True)
repositorio.buscar_por_id.return_value = {
    "id": 1,
    "email": "ana@example.com",
}

instance=True indica que queremos un mock que represente una instancia de la clase, no la clase como constructor.

17.11 Prueba con create_autospec

Función:

def obtener_email_usuario(usuario_id, repositorio):
    usuario = repositorio.buscar_por_id(usuario_id)

    if usuario is None:
        return None

    return usuario["email"]

Prueba:

def test_obtener_email_usuario_con_autospec():
    repositorio = create_autospec(RepositorioUsuarios, instance=True)
    repositorio.buscar_por_id.return_value = {
        "id": 1,
        "email": "ana@example.com",
    }

    email = obtener_email_usuario(1, repositorio)

    assert email == "ana@example.com"
    repositorio.buscar_por_id.assert_called_once_with(1)

Si escribimos repositorio.buscar en lugar de buscar_por_id, el mock fallará.

17.12 patch con autospec=True

Cuando usamos patch, podemos agregar autospec=True:

with patch("tienda.notificaciones.ServicioEmail", autospec=True) as ServicioEmailMock:
    instancia = ServicioEmailMock.return_value

Esto crea un mock basado en la clase real. Si llamamos métodos con firmas incorrectas, la prueba puede detectar el problema.

17.13 Ejemplo con patch autospec

Código:

from tienda.email import ServicioEmail


def enviar_bienvenida(usuario):
    servicio = ServicioEmail()
    servicio.enviar_bienvenida(usuario["email"])

Prueba:

from unittest.mock import patch


def test_enviar_bienvenida_con_autospec():
    usuario = {"email": "ana@example.com"}

    with patch("tienda.notificaciones.ServicioEmail", autospec=True) as ServicioEmailMock:
        instancia = ServicioEmailMock.return_value

        enviar_bienvenida(usuario)

    instancia.enviar_bienvenida.assert_called_once_with("ana@example.com")

El mock queda más alineado con la clase real.

17.14 autospec y self

Cuando se usa autospec sobre métodos o clases, Python tiene en cuenta self. En la mayoría de pruebas donde parcheamos una clase y usamos return_value, no necesitamos pasar self manualmente.

Si parcheas métodos directamente en la clase, presta atención a cómo se enlaza el método con la instancia.

17.15 Cuándo usar spec, spec_set o autospec

  • Usa spec cuando quieras evitar métodos inexistentes.
  • Usa spec_set cuando además quieras evitar asignar atributos nuevos.
  • Usa create_autospec cuando quieras respetar firmas de funciones o métodos.
  • Usa patch(..., autospec=True) cuando reemplaces clases o funciones con patch y quieras más seguridad.

17.16 Cuándo no exagerar

No todas las pruebas necesitan autospec. Si estás usando un stub manual pequeño o una dataclass como dato, no hace falta complicarlo.

Pero cuando un mock representa una dependencia real importante, especialmente una API interna o una clase con métodos definidos, autospec puede evitar errores costosos.

Mientras más importante sea la interfaz mockeada, más conviene restringir el mock con spec, spec_set o autospec.

17.17 Error frecuente: autospec sobre la ruta equivocada

autospec=True no corrige una ruta de patch incorrecta. Si parcheas el nombre equivocado, el código seguirá usando la dependencia real o el mock no será llamado.

Primero aplica la regla del tema anterior: parchea donde se usa la dependencia. Luego agrega autospec=True para mejorar la seguridad del mock.

17.18 Ejercicio práctico

Dada esta clase:

class PasarelaPago:
    def cobrar(self, tarjeta, total):
        pass

Y esta función:

def procesar_pago(pago, pasarela):
    return pasarela.cobrar(pago["tarjeta"], pago["total"])

Escribe una prueba usando create_autospec para simular la pasarela y verificar la llamada.

17.19 Solución posible del ejercicio

Una solución:

from unittest.mock import create_autospec

from pagos import PasarelaPago, procesar_pago


def test_procesar_pago_con_autospec():
    pasarela = create_autospec(PasarelaPago, instance=True)
    pasarela.cobrar.return_value = {"aprobado": True}
    pago = {
        "tarjeta": "4111111111111111",
        "total": 2500,
    }

    resultado = procesar_pago(pago, pasarela)

    assert resultado == {"aprobado": True}
    pasarela.cobrar.assert_called_once_with("4111111111111111", 2500)

Si la función intentara llamar a un método inexistente o con una firma incorrecta, el mock ayudaría a detectarlo.

17.20 Conclusión

Mock es flexible, pero esa flexibilidad puede ocultar errores. spec, spec_set, create_autospec y patch(..., autospec=True) permiten crear mocks más cercanos a las interfaces reales.

En el próximo tema veremos cómo mockear objetos con métodos mágicos, context managers e iteradores.