El error más común al usar patch es aplicarlo en el lugar equivocado. La regla práctica es: parchea el nombre que usa el código bajo prueba, no necesariamente el lugar donde la función o clase fue definida originalmente.
En este tema veremos esa regla con ejemplos concretos de imports en Python.
patch reemplaza nombres. Por eso debemos mirar el módulo que estamos probando y preguntarnos: ¿con qué nombre accede este código a la dependencia?
Si el código usa uuid4(), parcheamos el nombre uuid4 en ese módulo. Si usa uuid.uuid4(), parcheamos uuid.uuid4 visto desde ese módulo.
Archivo tienda/codigos.py:
from uuid import uuid4
def crear_codigo_pedido():
return f"PED-{uuid4()}"
Dentro de tienda.codigos, el código no usa uuid.uuid4. Usa el nombre local uuid4.
La prueba correcta parchea tienda.codigos.uuid4:
from unittest.mock import patch
from tienda.codigos import crear_codigo_pedido
def test_crear_codigo_pedido():
with patch("tienda.codigos.uuid4") as uuid4_mock:
uuid4_mock.return_value = "ABC123"
codigo = crear_codigo_pedido()
assert codigo == "PED-ABC123"
Reemplazamos exactamente el nombre que la función usa.
Esta prueba parece razonable, pero no afecta al código bajo prueba:
with patch("uuid.uuid4") as uuid4_mock:
uuid4_mock.return_value = "ABC123"
codigo = crear_codigo_pedido()
El módulo tienda.codigos ya tiene una referencia local llamada uuid4. Parchear uuid.uuid4 no cambia esa referencia local.
Ahora cambiemos el código:
import uuid
def crear_codigo_pedido():
return f"PED-{uuid.uuid4()}"
En este caso, el módulo usa el nombre uuid y luego accede a uuid4 dentro de ese módulo.
Podemos parchear el atributo uuid4 del módulo uuid usado por tienda.codigos:
def test_crear_codigo_pedido_con_import_uuid():
with patch("tienda.codigos.uuid.uuid4") as uuid4_mock:
uuid4_mock.return_value = "ABC123"
codigo = crear_codigo_pedido()
assert codigo == "PED-ABC123"
La ruta cambió porque cambió la forma de importar.
Archivo tienda/notificaciones.py:
from tienda.email import ServicioEmail
def enviar_bienvenida(usuario):
servicio = ServicioEmail()
servicio.enviar(usuario["email"], "Bienvenido")
El código usa el nombre local ServicioEmail dentro de tienda.notificaciones.
La prueba debe parchear tienda.notificaciones.ServicioEmail:
from unittest.mock import patch
from tienda.notificaciones import enviar_bienvenida
def test_enviar_bienvenida():
usuario = {"email": "ana@example.com"}
with patch("tienda.notificaciones.ServicioEmail") as ServicioEmailMock:
instancia = ServicioEmailMock.return_value
enviar_bienvenida(usuario)
ServicioEmailMock.assert_called_once_with()
instancia.enviar.assert_called_once_with("ana@example.com", "Bienvenido")
Parchear tienda.email.ServicioEmail no reemplazaría el nombre que usa tienda.notificaciones.
Si el código estuviera escrito así:
import tienda.email
def enviar_bienvenida(usuario):
servicio = tienda.email.ServicioEmail()
servicio.enviar(usuario["email"], "Bienvenido")
Entonces la ruta para patch sería distinta.
En ese caso, podríamos escribir:
def test_enviar_bienvenida_con_modulo_importado():
usuario = {"email": "ana@example.com"}
with patch("tienda.notificaciones.tienda.email.ServicioEmail") as ServicioEmailMock:
instancia = ServicioEmailMock.return_value
enviar_bienvenida(usuario)
instancia.enviar.assert_called_once_with("ana@example.com", "Bienvenido")
La ruta se ve más larga porque el módulo bajo prueba accede a la clase a través del nombre tienda.
Una práctica simple evita muchos errores: antes de escribir patch, abre el archivo que estás probando y mira exactamente cómo importa la dependencia.
from modulo import funcion, probablemente parcheas modulo_bajo_prueba.funcion.import modulo, probablemente parcheas modulo_bajo_prueba.modulo.funcion.as, debes parchear ese alias.Archivo tienda/reportes.py:
import uuid as generador_uuid
def crear_id_reporte():
return f"REP-{generador_uuid.uuid4()}"
El código no usa el nombre uuid. Usa el alias generador_uuid.
La prueba debe respetar el alias:
from unittest.mock import patch
from tienda.reportes import crear_id_reporte
def test_crear_id_reporte():
with patch("tienda.reportes.generador_uuid.uuid4") as uuid4_mock:
uuid4_mock.return_value = "XYZ"
reporte_id = crear_id_reporte()
assert reporte_id == "REP-XYZ"
Algunas señales de patch aplicado en el lugar equivocado:
Cuando pase esto, revisa el import del módulo bajo prueba.
Si esta aserción falla:
uuid4_mock.assert_called_once_with()
pero el código sí generó un identificador, probablemente se parcheó un nombre que el código no usa.
La solución no es quitar la aserción. La solución es encontrar el nombre correcto que debe reemplazarse.
Muchos problemas de patch desaparecen si el código recibe sus dependencias de forma explícita:
def crear_codigo_pedido(generar_uuid):
return f"PED-{generar_uuid()}"
La prueba ya no necesita patch:
def test_crear_codigo_pedido_sin_patch():
codigo = crear_codigo_pedido(lambda: "ABC123")
assert codigo == "PED-ABC123"
Esto no siempre es posible, especialmente con código existente, pero suele ser más simple.
Indica cuál es la ruta correcta para patch en este código ubicado en seguridad/tokens.py:
from secrets import token_hex
def crear_token_usuario(usuario_id):
return f"{usuario_id}-{token_hex(8)}"
Luego escribe la prueba para que token_hex(8) devuelva "abc123".
Como el módulo usa el nombre local token_hex, la ruta correcta es seguridad.tokens.token_hex:
from unittest.mock import patch
from seguridad.tokens import crear_token_usuario
def test_crear_token_usuario():
with patch("seguridad.tokens.token_hex") as token_hex_mock:
token_hex_mock.return_value = "abc123"
token = crear_token_usuario("USR-1")
assert token == "USR-1-abc123"
token_hex_mock.assert_called_once_with(8)
La regla central de patch es aplicar el reemplazo en el lugar donde el código bajo prueba usa la dependencia. Para eso hay que mirar el import real del módulo que estamos probando.
En el próximo tema veremos las distintas formas de aplicar patch: como decorador, context manager y fixture.