15. Aplicar patch como decorador, context manager y fixture

15.1 Objetivo del tema

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.

Objetivo práctico: elegir la forma más clara de aplicar patch según el tamaño y la intención de la prueba.

15.2 Caso base

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.

15.3 patch como context manager

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.

15.4 Ventajas del context manager

  • El alcance del patch queda visible.
  • Es fácil configurar el mock antes de ejecutar el código probado.
  • Funciona bien cuando el patch se usa solo en una parte de la prueba.
  • Evita confusiones con el orden de parámetros de los decoradores.

Para muchas pruebas, esta es la opción más legible.

15.5 patch como decorador

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.

15.6 Ventajas del decorador

  • Reduce indentación cuando toda la prueba necesita el patch.
  • Deja claro desde el encabezado qué dependencia se reemplaza.
  • Puede ser cómodo para pruebas cortas.

La desventaja aparece cuando hay varios decoradores: el orden de los parámetros puede volverse confuso.

15.7 Varios decoradores patch

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.

Con varios patches, considera usar context managers para que el orden sea más evidente.

15.8 patch con return_value en el decorador

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.

15.9 patch con new

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.

15.10 patch dentro de una fixture

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.

15.11 Usar la fixture

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.

15.12 Cuándo usar fixture

Una fixture con patch puede ser buena opción cuando:

  • Muchas pruebas necesitan el mismo reemplazo.
  • La configuración del mock requiere varios pasos.
  • Queremos dar un nombre claro al escenario compartido.
  • El patch es parte de un armado de prueba más grande.

No conviene ocultar demasiada configuración en fixtures si eso hace que la prueba sea difícil de entender.

15.13 Fixture con datos variables

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.

15.14 patch como start y stop

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.

15.15 Comparación rápida

  • Context manager: claro, explícito y recomendable para la mayoría de los casos.
  • Decorador: cómodo para pruebas cortas con uno o pocos patches.
  • Fixture: útil para compartir configuración entre varias pruebas.
  • start/stop: flexible, pero más fácil de usar mal si no se restaura el patch.

15.16 Evitar patches invisibles

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.

La comodidad de una fixture no debe ocultar información esencial del escenario de prueba.

15.17 Ejercicio práctico

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".

15.18 Solución posible del ejercicio

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)

15.19 Conclusión

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.