13. Usar patch para reemplazar funciones, clases y objetos durante una prueba

13.1 Objetivo del tema

Hasta ahora reemplazamos dependencias pasándolas como parámetros o constructores. Esa suele ser la opción más clara. Sin embargo, a veces el código ya importa o crea una dependencia internamente, y no podemos cambiarlo de inmediato.

En esos casos podemos usar patch, una herramienta de unittest.mock que reemplaza temporalmente un nombre durante una prueba.

Objetivo práctico: usar patch para reemplazar funciones, clases y objetos sin modificar permanentemente el código de producción.

13.2 Importar patch

patch forma parte de unittest.mock:

from unittest.mock import patch

También es habitual importarlo junto con Mock:

from unittest.mock import Mock, patch

13.3 Qué hace patch

patch reemplaza un nombre en un módulo durante un bloque de código, una función de prueba o una fixture. Al terminar, restaura el valor original.

Por ejemplo, puede reemplazar:

  • Una función importada.
  • Una clase que el código instancia.
  • Una constante de configuración.
  • Un objeto global.
  • Un método de una clase.

El reemplazo es temporal y controlado por la prueba.

13.4 Primer ejemplo: reemplazar una función

Supongamos un archivo tienda/codigos.py:

from uuid import uuid4


def generar_codigo_pedido():
    return f"PED-{uuid4()}"

La función usa uuid4, que genera un valor diferente en cada ejecución. Podemos reemplazarlo durante la prueba.

13.5 patch como context manager

Usando patch con with:

from unittest.mock import patch

from tienda.codigos import generar_codigo_pedido


def test_generar_codigo_pedido():
    with patch("tienda.codigos.uuid4") as uuid4_mock:
        uuid4_mock.return_value = "ABC123"

        codigo = generar_codigo_pedido()

    assert codigo == "PED-ABC123"

Durante el bloque with, el nombre uuid4 dentro de tienda.codigos queda reemplazado por un mock.

13.6 El string de patch

El argumento de patch es una cadena con la ruta del nombre que queremos reemplazar:

patch("tienda.codigos.uuid4")

No estamos reemplazando uuid.uuid4 globalmente. Estamos reemplazando el nombre uuid4 que el módulo tienda.codigos usa.

patch reemplaza nombres, no objetos abstractos. Debes apuntar al nombre que usa el código bajo prueba.

13.7 Reemplazar una función de fecha

Archivo tienda/facturas.py:

from datetime import date


def crear_numero_factura(cliente_id):
    hoy = date.today()
    return f"FAC-{hoy.year}-{cliente_id}"

Queremos controlar la fecha para que la prueba no dependa del día actual.

13.8 patch de date

Podemos reemplazar date en el módulo donde se usa:

from datetime import date
from unittest.mock import patch

from tienda.facturas import crear_numero_factura


def test_crear_numero_factura():
    with patch("tienda.facturas.date") as date_mock:
        date_mock.today.return_value = date(2026, 5, 15)

        numero = crear_numero_factura("CLI-10")

    assert numero == "FAC-2026-CLI-10"

El código de producción sigue usando date.today(), pero la prueba controla qué fecha devuelve.

13.9 Reemplazar una clase instanciada internamente

Supongamos este código:

from tienda.email import ServicioEmail


def enviar_bienvenida(usuario):
    email = ServicioEmail()
    email.enviar(
        destino=usuario["email"],
        asunto="Bienvenido",
    )

La función crea internamente ServicioEmail. Si no podemos refactorizar todavía, usamos patch.

13.10 patch de clase

Cuando parcheamos una clase, el mock reemplaza la clase. Su return_value representa la instancia creada al llamar la clase.

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_email = ServicioEmailMock.return_value

        enviar_bienvenida(usuario)

    ServicioEmailMock.assert_called_once_with()
    instancia_email.enviar.assert_called_once_with(
        destino="ana@example.com",
        asunto="Bienvenido",
    )

Este patrón es muy común cuando el código instancia una clase dentro de la función probada.

13.11 Reemplazar un objeto por uno propio

No siempre queremos que patch cree un mock automático. Podemos indicar el reemplazo con new:

def funcion_falsa():
    return "valor controlado"


with patch("modulo.funcion_original", new=funcion_falsa):
    resultado = codigo_que_usa_funcion_original()

Esto sirve para reemplazar una función por una implementación simple.

13.12 Ejemplo con new

Archivo tienda/configuracion.py:

MODO_DEBUG = False


def mostrar_detalle_error():
    return MODO_DEBUG

Prueba:

from unittest.mock import patch

from tienda.configuracion import mostrar_detalle_error


def test_mostrar_detalle_error_en_debug():
    with patch("tienda.configuracion.MODO_DEBUG", new=True):
        assert mostrar_detalle_error() is True

La constante cambia solo dentro del bloque with.

13.13 patch.object

patch.object permite reemplazar un atributo de un objeto o clase ya importado:

from unittest.mock import patch


with patch.object(objeto, "metodo") as metodo_mock:
    metodo_mock.return_value = "ok"

Puede ser útil cuando tenemos una referencia directa al objeto y queremos reemplazar uno de sus atributos.

13.14 Ejemplo con patch.object

Supongamos una clase:

class CalculadoraImpuestos:
    def obtener_tasa(self):
        return 0.21

    def calcular_iva(self, total):
        return total * self.obtener_tasa()

Podemos reemplazar el método obtener_tasa para una prueba:

def test_calcular_iva_con_tasa_controlada():
    calculadora = CalculadoraImpuestos()

    with patch.object(calculadora, "obtener_tasa", return_value=0.10):
        iva = calculadora.calcular_iva(1000)

    assert iva == 100

Esta técnica debe usarse con criterio, porque parchear métodos del mismo objeto puede acoplar la prueba a detalles internos.

13.15 Cuándo usar patch

patch suele ser útil cuando:

  • El código usa una función de tiempo, aleatoriedad o identificadores.
  • La dependencia se instancia dentro del código y aún no se puede refactorizar.
  • Queremos reemplazar una constante o configuración global durante una prueba.
  • Necesitamos aislar una llamada externa de un módulo existente.

Si puedes pasar la dependencia explícitamente sin complicar el diseño, la inyección de dependencias suele ser más clara.

13.16 Cuándo evitar patch

Evita usar patch como primera opción si el código puede recibir sus dependencias por parámetro o constructor. Muchos parches en una prueba suelen indicar acoplamiento excesivo.

También conviene evitar parches sobre detalles internos que no forman parte del comportamiento que queremos verificar.

patch es muy útil para trabajar con código existente, pero no debe ocultar problemas de diseño que podrían resolverse con dependencias explícitas.

13.17 Ejercicio práctico

Prueba esta función reemplazando uuid4 con patch:

from uuid import uuid4


def crear_id_sesion(usuario_id):
    return f"SES-{usuario_id}-{uuid4()}"

La prueba debe verificar que el identificador generado sea predecible.

13.18 Solución posible del ejercicio

Si la función está en seguridad/sesiones.py, la prueba puede ser:

from unittest.mock import patch

from seguridad.sesiones import crear_id_sesion


def test_crear_id_sesion():
    with patch("seguridad.sesiones.uuid4") as uuid4_mock:
        uuid4_mock.return_value = "ABC123"

        sesion_id = crear_id_sesion("USR-1")

    assert sesion_id == "SES-USR-1-ABC123"
    uuid4_mock.assert_called_once_with()

El punto importante es parchear seguridad.sesiones.uuid4, porque ese es el nombre que usa el código bajo prueba.

13.19 Conclusión

patch permite reemplazar temporalmente funciones, clases, constantes y objetos durante una prueba. Es especialmente útil para controlar tiempo, aleatoriedad, identificadores y dependencias creadas internamente.

En el próximo tema veremos con más detalle el punto que más errores genera: dónde aplicar patch.