pytest ofrece herramientas muy útiles para pruebas con dobles. Una de ellas es monkeypatch, una fixture integrada que permite reemplazar atributos, variables de entorno y elementos de diccionarios de forma temporal.
En este tema veremos cómo usar monkeypatch, cómo combinarlo con fixtures propias y cómo parametrizar pruebas para cubrir varios escenarios sin duplicar código.
monkeypatch es una fixture de pytest. No hay que importarla: se pide como parámetro en la prueba.
def test_algo(monkeypatch):
...
Permite modificar temporalmente el entorno de ejecución. Al terminar la prueba, pytest revierte los cambios automáticamente.
setattr reemplaza un atributo de un módulo, clase u objeto. Es parecido a patch, pero con estilo de pytest.
Supongamos tienda/codigos.py:
from uuid import uuid4
def crear_codigo_pedido():
return f"PED-{uuid4()}"
Podemos reemplazar uuid4 así:
import tienda.codigos as codigos
def test_crear_codigo_pedido(monkeypatch):
monkeypatch.setattr(codigos, "uuid4", lambda: "ABC123")
assert codigos.crear_codigo_pedido() == "PED-ABC123"
En el ejemplo anterior importamos el módulo completo como codigos. Eso deja muy claro qué atributo estamos reemplazando:
monkeypatch.setattr(codigos, "uuid4", lambda: "ABC123")
La misma regla de patch sigue vigente: reemplazamos el nombre usado por el módulo bajo prueba.
Podemos reemplazar una función real por una función fake:
def uuid_fijo():
return "ABC123"
def test_crear_codigo_pedido_con_funcion_fake(monkeypatch):
monkeypatch.setattr(codigos, "uuid4", uuid_fijo)
assert codigos.crear_codigo_pedido() == "PED-ABC123"
Esto es claro cuando no necesitamos verificar llamadas. Si necesitamos verificar llamadas, podemos usar un Mock.
También podemos reemplazar por un mock:
from unittest.mock import Mock
def test_crear_codigo_pedido_con_mock(monkeypatch):
uuid4_mock = Mock(return_value="ABC123")
monkeypatch.setattr(codigos, "uuid4", uuid4_mock)
assert codigos.crear_codigo_pedido() == "PED-ABC123"
uuid4_mock.assert_called_once_with()
Esta combinación es útil cuando queremos usar el estilo de restauración de pytest y las aserciones de Mock.
setenv modifica variables de entorno durante la prueba. Supongamos:
import os
def obtener_modo():
return os.getenv("APP_MODE", "prod")
Prueba:
def test_obtener_modo_desde_variable_de_entorno(monkeypatch):
monkeypatch.setenv("APP_MODE", "test")
assert obtener_modo() == "test"
Al terminar la prueba, pytest restaura el entorno.
Para simular que una variable no existe, usamos delenv:
def test_obtener_modo_por_defecto(monkeypatch):
monkeypatch.delenv("APP_MODE", raising=False)
assert obtener_modo() == "prod"
raising=False evita que la prueba falle si la variable no estaba definida.
También se pueden modificar diccionarios temporalmente:
CONFIG = {
"moneda": "USD",
}
def obtener_moneda():
return CONFIG["moneda"]
Prueba:
def test_obtener_moneda(monkeypatch):
monkeypatch.setitem(CONFIG, "moneda", "ARS")
assert obtener_moneda() == "ARS"
Podemos crear fixtures para stubs, fakes o mocks usados en varias pruebas:
import pytest
class RepositorioClientesStub:
def __init__(self, cliente):
self.cliente = cliente
def buscar_por_id(self, cliente_id):
return self.cliente
@pytest.fixture
def cliente_vip():
return {"id": 1, "nombre": "Ana", "categoria": "vip"}
Luego usamos la fixture en la prueba.
Ejemplo:
@pytest.fixture
def repositorio_cliente_vip(cliente_vip):
return RepositorioClientesStub(cliente_vip)
def test_cliente_vip_tiene_descuento(repositorio_cliente_vip):
descuento = calcular_descuento(1, 1000, repositorio_cliente_vip)
assert descuento == 200
Las fixtures ayudan a nombrar escenarios comunes, pero no conviene esconder datos importantes en exceso.
pytest.mark.parametrize permite ejecutar la misma prueba con varios datos:
import pytest
@pytest.mark.parametrize(
"categoria,total,descuento_esperado",
[
("vip", 1000, 200),
("frecuente", 1000, 100),
("comun", 1000, 0),
],
)
def test_calcular_descuento_por_categoria(categoria, total, descuento_esperado):
repositorio = RepositorioClientesStub({
"id": 1,
"nombre": "Ana",
"categoria": categoria,
})
descuento = calcular_descuento(1, total, repositorio)
assert descuento == descuento_esperado
Esto evita duplicar pruebas muy parecidas.
También podemos parametrizar escenarios de error:
def validar_total(total):
if total <= 0:
raise ValueError("El total debe ser mayor que cero")
return total
@pytest.mark.parametrize("total_invalido", [0, -1, -100])
def test_validar_total_rechaza_valores_invalidos(total_invalido):
with pytest.raises(ValueError):
validar_total(total_invalido)
La parametrización es útil cuando cambia el dato, pero la intención de la prueba es la misma.
Ambas herramientas permiten reemplazar dependencias temporalmente. Algunas diferencias prácticas:
patch es parte de unittest.mock y crea mocks automáticamente.monkeypatch es una fixture de pytest y puede reemplazar por cualquier objeto.patch se usa mucho con context managers y decoradores.monkeypatch se integra naturalmente con fixtures de pytest.monkeypatch suele ser cómodo cuando:
pytest para armar el escenario.Si necesitas muchas aserciones de llamadas, puedes combinarlo con Mock.
Si el código ya permite inyectar la dependencia por parámetro, no hace falta usar monkeypatch:
def crear_codigo(generar_token):
return f"COD-{generar_token()}"
La prueba puede pasar una función directamente:
def test_crear_codigo_sin_monkeypatch():
assert crear_codigo(lambda: "ABC") == "COD-ABC"
Usa la herramienta más simple que resuelva el problema.
Este código está en seguridad/tokens.py:
from secrets import token_hex
def crear_token():
return token_hex(4)
Escribe una prueba con monkeypatch para que crear_token() devuelva "abcd". Luego escribe otra usando Mock para verificar que token_hex fue llamado con 4.
Solución con función fake:
import seguridad.tokens as tokens
def test_crear_token_con_monkeypatch(monkeypatch):
monkeypatch.setattr(tokens, "token_hex", lambda cantidad: "abcd")
assert tokens.crear_token() == "abcd"
Solución con Mock:
from unittest.mock import Mock
import seguridad.tokens as tokens
def test_crear_token_verifica_llamada(monkeypatch):
token_hex_mock = Mock(return_value="abcd")
monkeypatch.setattr(tokens, "token_hex", token_hex_mock)
assert tokens.crear_token() == "abcd"
token_hex_mock.assert_called_once_with(4)
pytest facilita el trabajo con dobles mediante monkeypatch, fixtures y parametrización. Estas herramientas permiten reemplazar dependencias, preparar escenarios reutilizables y cubrir varios casos con menos duplicación.
En el próximo tema veremos cómo controlar variables de entorno y configuración en las pruebas con más profundidad.