monkeypatch es una fixture de pytest que permite modificar temporalmente funciones, atributos, variables de entorno y el directorio de trabajo durante una prueba.
Al terminar la prueba, pytest revierte automáticamente esos cambios.
monkeypatch cambia algo solo durante una prueba y luego lo deja como estaba.
Crea un proyecto nuevo:
mkdir pytest-monkeypatch-demo
cd pytest-monkeypatch-demo
Si pytest no está instalado en el entorno activo:
python -m pip install pytest
Crea un archivo llamado configuracion.py:
import os
from pathlib import Path
def obtener_modo():
return os.getenv("APP_MODO", "desarrollo")
def obtener_api_key():
api_key = os.getenv("API_KEY")
if api_key is None:
raise RuntimeError("Falta API_KEY")
return api_key
def ruta_configuracion():
return Path.cwd() / "config.ini"
Este módulo lee variables de entorno y usa el directorio actual.
Crea un archivo llamado usuarios.py:
def obtener_usuario_desde_api(usuario_id):
raise RuntimeError("No llamar a la API real durante una prueba")
def normalizar_nombre(nombre):
return nombre.strip().title()
def construir_perfil(usuario_id):
usuario = obtener_usuario_desde_api(usuario_id)
return {
"id": usuario["id"],
"nombre": normalizar_nombre(usuario["nombre"]),
"activo": usuario["activo"],
}
class ClienteEmail:
servidor = "smtp.real.example.com"
def enviar(self, destinatario, asunto):
raise RuntimeError("No enviar emails reales durante una prueba")
def notificar_bienvenida(cliente_email, email):
cliente_email.enviar(email, "Bienvenido")
return True
En las pruebas reemplazaremos la función que consulta la API y algunos atributos del cliente de email.
monkeypatch.setenv define una variable de entorno solo durante la prueba:
from configuracion import obtener_modo
def test_obtener_modo_desde_variable_de_entorno(monkeypatch):
monkeypatch.setenv("APP_MODO", "produccion")
assert obtener_modo() == "produccion"
Al terminar la prueba, APP_MODO vuelve a su estado anterior.
Para probar el valor por defecto, podemos asegurarnos de que la variable no exista:
def test_obtener_modo_por_defecto(monkeypatch):
monkeypatch.delenv("APP_MODO", raising=False)
assert obtener_modo() == "desarrollo"
raising=False evita error si la variable ya no estaba definida.
También podemos probar una configuración obligatoria:
import pytest
from configuracion import obtener_api_key
def test_obtener_api_key_faltante_lanza_error(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(RuntimeError):
obtener_api_key()
monkeypatch.chdir cambia temporalmente la carpeta actual:
from configuracion import ruta_configuracion
def test_ruta_configuracion_usa_directorio_actual(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
assert ruta_configuracion() == tmp_path / "config.ini"
Esto es útil para probar código que usa Path.cwd() o rutas relativas.
Con monkeypatch.setattr podemos reemplazar una función por otra durante la prueba:
import usuarios
def test_construir_perfil_reemplaza_api(monkeypatch):
def obtener_usuario_falso(usuario_id):
return {
"id": usuario_id,
"nombre": " ana ",
"activo": True,
}
monkeypatch.setattr(usuarios, "obtener_usuario_desde_api", obtener_usuario_falso)
perfil = usuarios.construir_perfil(10)
assert perfil == {
"id": 10,
"nombre": "Ana",
"activo": True,
}
La API real no se llama. La prueba controla la respuesta.
Si la sustitución es pequeña, una lambda puede ser suficiente:
def test_construir_perfil_usuario_inactivo(monkeypatch):
monkeypatch.setattr(
usuarios,
"obtener_usuario_desde_api",
lambda usuario_id: {
"id": usuario_id,
"nombre": "Luis",
"activo": False,
},
)
perfil = usuarios.construir_perfil(20)
assert perfil["activo"] is False
monkeypatch.setattr también sirve para atributos:
from usuarios import ClienteEmail
def test_reemplazar_servidor_de_email(monkeypatch):
monkeypatch.setattr(ClienteEmail, "servidor", "smtp.test.local")
cliente = ClienteEmail()
assert cliente.servidor == "smtp.test.local"
El cambio se revierte automáticamente al finalizar la prueba.
Podemos reemplazar un método para evitar acciones reales:
from usuarios import notificar_bienvenida
def test_notificar_bienvenida(monkeypatch):
enviados = []
def enviar_falso(self, destinatario, asunto):
enviados.append({
"destinatario": destinatario,
"asunto": asunto,
})
monkeypatch.setattr(ClienteEmail, "enviar", enviar_falso)
cliente = ClienteEmail()
resultado = notificar_bienvenida(cliente, "ana@example.com")
assert resultado is True
assert enviados == [
{
"destinatario": "ana@example.com",
"asunto": "Bienvenido",
}
]
El método falso recibe self igual que un método normal.
patch y monkeypatch resuelven problemas parecidos, pero tienen estilos distintos:
patch viene de unittest.mock y crea mocks con métodos de verificación.monkeypatch es una fixture de pytest y reemplaza valores directamente.monkeypatch es muy cómodo para variables de entorno, directorios y reemplazos simples.El archivo test_monkeypatch.py puede quedar así:
import pytest
import usuarios
from configuracion import obtener_api_key, obtener_modo, ruta_configuracion
from usuarios import ClienteEmail, notificar_bienvenida
def test_obtener_modo_desde_variable_de_entorno(monkeypatch):
monkeypatch.setenv("APP_MODO", "produccion")
assert obtener_modo() == "produccion"
def test_obtener_modo_por_defecto(monkeypatch):
monkeypatch.delenv("APP_MODO", raising=False)
assert obtener_modo() == "desarrollo"
def test_obtener_api_key_faltante_lanza_error(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(RuntimeError):
obtener_api_key()
def test_ruta_configuracion_usa_directorio_actual(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
assert ruta_configuracion() == tmp_path / "config.ini"
def test_construir_perfil_reemplaza_api(monkeypatch):
def obtener_usuario_falso(usuario_id):
return {
"id": usuario_id,
"nombre": " ana ",
"activo": True,
}
monkeypatch.setattr(usuarios, "obtener_usuario_desde_api", obtener_usuario_falso)
perfil = usuarios.construir_perfil(10)
assert perfil == {
"id": 10,
"nombre": "Ana",
"activo": True,
}
def test_construir_perfil_usuario_inactivo(monkeypatch):
monkeypatch.setattr(
usuarios,
"obtener_usuario_desde_api",
lambda usuario_id: {
"id": usuario_id,
"nombre": "Luis",
"activo": False,
},
)
perfil = usuarios.construir_perfil(20)
assert perfil["activo"] is False
def test_reemplazar_servidor_de_email(monkeypatch):
monkeypatch.setattr(ClienteEmail, "servidor", "smtp.test.local")
cliente = ClienteEmail()
assert cliente.servidor == "smtp.test.local"
def test_notificar_bienvenida(monkeypatch):
enviados = []
def enviar_falso(self, destinatario, asunto):
enviados.append({
"destinatario": destinatario,
"asunto": asunto,
})
monkeypatch.setattr(ClienteEmail, "enviar", enviar_falso)
cliente = ClienteEmail()
resultado = notificar_bienvenida(cliente, "ana@example.com")
assert resultado is True
assert enviados == [
{
"destinatario": "ana@example.com",
"asunto": "Bienvenido",
}
]
Desde la raíz del proyecto, ejecuta:
python -m pytest
La salida esperada será similar a:
collected 8 items
test_monkeypatch.py ........ [100%]
8 passed in 0.05s
monkeypatch.setattr(objeto, nombre, valor): reemplaza un atributo.monkeypatch.delattr(objeto, nombre): elimina temporalmente un atributo.monkeypatch.setenv(nombre, valor): define una variable de entorno.monkeypatch.delenv(nombre, raising=False): elimina una variable de entorno.monkeypatch.chdir(ruta): cambia el directorio de trabajo.patch, debes cambiar lo que usa el módulo bajo prueba.raising=False: al eliminar una variable que puede no existir, evita errores innecesarios.self: un método falso de instancia debe aceptar self.mkdir pytest-monkeypatch-demo
cd pytest-monkeypatch-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_monkeypatch.py::test_construir_perfil_reemplaza_api -v
monkeypatch es una fixture de pytest.self.En este tema usamos monkeypatch para controlar el entorno de ejecución de una prueba: variables de entorno, rutas, funciones, atributos y métodos.
En el próximo tema aplicaremos estas ideas a módulos que usan requests, tiempo o variables de entorno.