14. Dónde aplicar patch: el lugar donde se usa la dependencia

14.1 Objetivo del tema

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.

Objetivo práctico: identificar la ruta correcta para patch según cómo el código importó la dependencia.

14.2 La regla principal

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.

No parchees donde la dependencia nació. Parchea donde el código bajo prueba la busca.

14.3 Caso con from uuid import uuid4

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.

14.4 Patch correcto para from import

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.

14.5 Patch incorrecto para este caso

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.

14.6 Caso con import uuid

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.

14.7 Patch correcto para import 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.

14.8 Caso con clase importada

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.

14.9 Patch correcto de clase importada

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.

14.10 Caso con módulo importado

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.

14.11 Patch correcto con módulo importado

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.

14.12 Leer el import antes de escribir el patch

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.

  • Si dice from modulo import funcion, probablemente parcheas modulo_bajo_prueba.funcion.
  • Si dice import modulo, probablemente parcheas modulo_bajo_prueba.modulo.funcion.
  • Si la dependencia se asigna a otro nombre con as, debes parchear ese alias.

14.13 Caso con 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.

14.14 Patch correcto con alias

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"

14.15 Cómo detectar que patch está mal aplicado

Algunas señales de patch aplicado en el lugar equivocado:

  • La prueba sigue llamando a la dependencia real.
  • El mock aparece como no llamado aunque el código sí ejecutó la funcionalidad.
  • El valor aleatorio o la fecha siguen cambiando.
  • La prueba falla con errores de red, base de datos o archivos reales.

Cuando pase esto, revisa el import del módulo bajo prueba.

14.16 Ejemplo de diagnóstico

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.

14.17 Preferir dependencias explícitas cuando sea posible

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.

14.18 Ejercicio práctico

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

14.19 Solución posible del ejercicio

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)

14.20 Conclusión

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.