patch puede usarse de varias formas. Las más comunes son como context manager, como decorador y dentro de una fixture de pytest.
El comportamiento de fondo es el mismo: reemplazar temporalmente un nombre y restaurarlo después. Lo que cambia es la legibilidad y el alcance del reemplazo.
Usaremos este archivo tienda/sesiones.py:
from uuid import uuid4
def crear_sesion(usuario_id):
return {
"usuario_id": usuario_id,
"sesion_id": f"SES-{uuid4()}",
}
Queremos controlar uuid4 para que la prueba tenga un resultado predecible.
El context manager usa with. Es la forma más explícita y suele ser la más clara para empezar:
from unittest.mock import patch
from tienda.sesiones import crear_sesion
def test_crear_sesion_con_context_manager():
with patch("tienda.sesiones.uuid4") as uuid4_mock:
uuid4_mock.return_value = "ABC123"
sesion = crear_sesion("USR-1")
assert sesion == {
"usuario_id": "USR-1",
"sesion_id": "SES-ABC123",
}
uuid4_mock.assert_called_once_with()
El parche está activo solo dentro del bloque with.
Para muchas pruebas, esta es la opción más legible.
patch también puede aplicarse como decorador de la función de prueba:
from unittest.mock import patch
@patch("tienda.sesiones.uuid4")
def test_crear_sesion_con_decorador(uuid4_mock):
uuid4_mock.return_value = "ABC123"
sesion = crear_sesion("USR-1")
assert sesion["sesion_id"] == "SES-ABC123"
uuid4_mock.assert_called_once_with()
El mock creado por patch se recibe como argumento de la prueba.
La desventaja aparece cuando hay varios decoradores: el orden de los parámetros puede volverse confuso.
Si usamos varios decoradores, los mocks se pasan a la función en orden inverso al de los decoradores:
@patch("tienda.sesiones.registrar_auditoria")
@patch("tienda.sesiones.uuid4")
def test_con_varios_patches(uuid4_mock, auditoria_mock):
uuid4_mock.return_value = "ABC123"
# cuerpo de la prueba
El decorador más cercano a la función entrega el primer argumento. Esto puede generar errores si no se lee con cuidado.
Podemos configurar el valor de retorno directamente:
@patch("tienda.sesiones.uuid4", return_value="ABC123")
def test_crear_sesion_configurado_en_decorador(uuid4_mock):
sesion = crear_sesion("USR-1")
assert sesion["sesion_id"] == "SES-ABC123"
uuid4_mock.assert_called_once_with()
Esto funciona bien cuando la configuración es simple.
Si no necesitamos un mock, podemos reemplazar por una función propia usando new:
def uuid_fijo():
return "ABC123"
def test_crear_sesion_con_new():
with patch("tienda.sesiones.uuid4", new=uuid_fijo):
sesion = crear_sesion("USR-1")
assert sesion["sesion_id"] == "SES-ABC123"
En este caso no podemos usar assert_called_once_with, porque reemplazamos por una función normal, no por un mock.
Si varias pruebas necesitan el mismo patch, podemos encapsularlo en una fixture de pytest:
import pytest
from unittest.mock import patch
@pytest.fixture
def uuid4_mock():
with patch("tienda.sesiones.uuid4") as mock:
mock.return_value = "ABC123"
yield mock
El patch estará activo durante la prueba que use la fixture y se restaurará al terminar.
La prueba queda así:
def test_crear_sesion_con_fixture(uuid4_mock):
sesion = crear_sesion("USR-1")
assert sesion["sesion_id"] == "SES-ABC123"
uuid4_mock.assert_called_once_with()
La fixture ayuda a evitar duplicación, pero debe usarse con cuidado para que la prueba siga siendo fácil de leer.
Una fixture con patch puede ser buena opción cuando:
No conviene ocultar demasiada configuración en fixtures si eso hace que la prueba sea difícil de entender.
Si cada prueba necesita configurar el mock de manera distinta, la fixture puede devolver el mock sin fijar el valor:
@pytest.fixture
def uuid4_mock():
with patch("tienda.sesiones.uuid4") as mock:
yield mock
def test_crear_sesion_con_valor_a(uuid4_mock):
uuid4_mock.return_value = "A"
sesion = crear_sesion("USR-1")
assert sesion["sesion_id"] == "SES-A"
def test_crear_sesion_con_valor_b(uuid4_mock):
uuid4_mock.return_value = "B"
sesion = crear_sesion("USR-2")
assert sesion["sesion_id"] == "SES-B"
Así cada prueba conserva control sobre su escenario.
patch también puede iniciarse y detenerse manualmente con start y stop:
patcher = patch("tienda.sesiones.uuid4")
uuid4_mock = patcher.start()
try:
uuid4_mock.return_value = "ABC123"
sesion = crear_sesion("USR-1")
finally:
patcher.stop()
En pruebas comunes con pytest, suele ser más claro usar context manager, decorador o fixture con yield.
Una prueba debe dejar claro qué está reemplazando. Si una fixture aplica muchos patches sin que el cuerpo de la prueba lo muestre, puede ser difícil entender por qué el código se comporta de cierta manera.
Prueba esta función usando patch como context manager y luego como decorador:
from secrets import token_hex
def crear_codigo_verificacion(usuario_id):
return f"VER-{usuario_id}-{token_hex(4)}"
Supón que la función está en seguridad/verificacion.py. Haz que token_hex(4) devuelva "abcd".
Con context manager:
from unittest.mock import patch
from seguridad.verificacion import crear_codigo_verificacion
def test_crear_codigo_verificacion_con_context_manager():
with patch("seguridad.verificacion.token_hex") as token_hex_mock:
token_hex_mock.return_value = "abcd"
codigo = crear_codigo_verificacion("USR-1")
assert codigo == "VER-USR-1-abcd"
token_hex_mock.assert_called_once_with(4)
Con decorador:
@patch("seguridad.verificacion.token_hex", return_value="abcd")
def test_crear_codigo_verificacion_con_decorador(token_hex_mock):
codigo = crear_codigo_verificacion("USR-1")
assert codigo == "VER-USR-1-abcd"
token_hex_mock.assert_called_once_with(4)
patch puede aplicarse como context manager, decorador o fixture. La mejor opción depende de la claridad que aporte a la prueba y del alcance que necesite el reemplazo.
En el próximo tema veremos errores comunes con imports, módulos y nombres parcheados.