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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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á.
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.
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.
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.
spec cuando quieras evitar métodos inexistentes.spec_set cuando además quieras evitar asignar atributos nuevos.create_autospec cuando quieras respetar firmas de funciones o métodos.patch(..., autospec=True) cuando reemplaces clases o funciones con patch y quieras más seguridad.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.
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.
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.
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.
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.