Muchas aplicaciones leen configuración desde variables de entorno, archivos, constantes o diccionarios globales. En las pruebas necesitamos controlar esos valores para que el resultado sea predecible.
En este tema veremos cómo probar código que depende de configuración usando monkeypatch, patch e inyección explícita de configuración.
Supongamos esta función:
import os
def obtener_url_api():
return os.getenv("API_URL", "https://api.example.com")
Si la prueba depende del valor real de API_URL en la máquina, puede pasar en un entorno y fallar en otro.
Con monkeypatch.setenv podemos fijar el valor durante la prueba:
from configuracion import obtener_url_api
def test_obtener_url_api_desde_entorno(monkeypatch):
monkeypatch.setenv("API_URL", "https://api.test.local")
assert obtener_url_api() == "https://api.test.local"
Al terminar la prueba, pytest restaura el entorno automáticamente.
Para probar el valor por defecto, conviene eliminar la variable:
def test_obtener_url_api_por_defecto(monkeypatch):
monkeypatch.delenv("API_URL", raising=False)
assert obtener_url_api() == "https://api.example.com"
raising=False evita que la prueba falle si la variable no existía.
Las variables de entorno son texto. Si necesitamos números o booleanos, el código debe convertirlos:
import os
def obtener_timeout():
return int(os.getenv("API_TIMEOUT", "5"))
Prueba:
def test_obtener_timeout(monkeypatch):
monkeypatch.setenv("API_TIMEOUT", "15")
assert obtener_timeout() == 15
También conviene probar el valor por defecto y valores inválidos si el código los maneja.
Podemos hacer que el código traduzca un valor inválido a un error claro:
class ConfiguracionInvalidaError(Exception):
pass
def obtener_timeout():
valor = os.getenv("API_TIMEOUT", "5")
try:
return int(valor)
except ValueError as error:
raise ConfiguracionInvalidaError(
"API_TIMEOUT debe ser un número entero"
) from error
Prueba:
import pytest
def test_obtener_timeout_invalido(monkeypatch):
monkeypatch.setenv("API_TIMEOUT", "rapido")
with pytest.raises(ConfiguracionInvalidaError):
obtener_timeout()
Para booleanos conviene definir una regla explícita:
def debug_activado():
return os.getenv("DEBUG", "false").lower() == "true"
Pruebas parametrizadas:
import pytest
@pytest.mark.parametrize(
"valor,esperado",
[
("true", True),
("TRUE", True),
("false", False),
("no", False),
],
)
def test_debug_activado(monkeypatch, valor, esperado):
monkeypatch.setenv("DEBUG", valor)
assert debug_activado() is esperado
Un error frecuente es leer variables de entorno al cargar el módulo:
import os
API_URL = os.getenv("API_URL", "https://api.example.com")
def obtener_url_api():
return API_URL
Si una prueba cambia API_URL con monkeypatch.setenv después de importar el módulo, la constante ya fue calculada.
Una versión más fácil de probar lee el entorno cuando se llama la función:
def obtener_url_api():
return os.getenv("API_URL", "https://api.example.com")
Así monkeypatch.setenv afecta la ejecución de la prueba de forma directa.
Si el código ya usa una constante, podemos reemplazarla con monkeypatch.setattr:
# configuracion.py
API_URL = "https://api.example.com"
def construir_endpoint(ruta):
return f"{API_URL}/{ruta}"
Prueba:
import configuracion
def test_construir_endpoint(monkeypatch):
monkeypatch.setattr(configuracion, "API_URL", "https://api.test.local")
assert configuracion.construir_endpoint("usuarios") == (
"https://api.test.local/usuarios"
)
Algunos proyectos usan un diccionario de configuración:
CONFIG = {
"moneda": "USD",
"iva": 0.21,
}
def calcular_iva(total):
return total * CONFIG["iva"]
Podemos modificarlo temporalmente con setitem:
def test_calcular_iva_con_configuracion_controlada(monkeypatch):
monkeypatch.setitem(CONFIG, "iva", 0.10)
assert calcular_iva(1000) == 100
Muchas veces es mejor recibir la configuración como argumento:
def calcular_iva(total, configuracion):
return total * configuracion["iva"]
La prueba no necesita monkeypatch:
def test_calcular_iva_con_configuracion_inyectada():
configuracion = {"iva": 0.10}
assert calcular_iva(1000, configuracion) == 100
Esta forma es simple, explícita y evita estado global.
Para configuraciones más estructuradas, una dataclass puede ser más clara:
from dataclasses import dataclass
@dataclass
class ConfiguracionPagos:
moneda: str
iva: float
def calcular_total_con_iva(total, configuracion):
return {
"moneda": configuracion.moneda,
"total": total + total * configuracion.iva,
}
Prueba:
def test_calcular_total_con_iva():
configuracion = ConfiguracionPagos(moneda="ARS", iva=0.21)
resultado = calcular_total_con_iva(1000, configuracion)
assert resultado == {"moneda": "ARS", "total": 1210}
Si varias pruebas usan la misma configuración, podemos crear una fixture:
import pytest
@pytest.fixture
def configuracion_test():
return ConfiguracionPagos(moneda="ARS", iva=0.21)
def test_calcular_total_con_fixture(configuracion_test):
resultado = calcular_total_con_iva(1000, configuracion_test)
assert resultado["total"] == 1210
La fixture da un nombre al escenario compartido.
Las pruebas no deben depender de claves reales. Si el código necesita una clave de API, usa un valor falso controlado:
def obtener_api_key():
api_key = os.getenv("API_KEY")
if not api_key:
raise ConfiguracionInvalidaError("Falta API_KEY")
return api_key
Prueba:
def test_obtener_api_key(monkeypatch):
monkeypatch.setenv("API_KEY", "clave-falsa")
assert obtener_api_key() == "clave-falsa"
También conviene probar el caso donde falta la variable:
def test_obtener_api_key_faltante(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(ConfiguracionInvalidaError):
obtener_api_key()
Este tipo de prueba evita que la aplicación falle con errores poco claros al iniciar.
monkeypatch.setenv y delenv para controlar el entorno.Prueba esta función:
import os
def obtener_configuracion_email():
servidor = os.getenv("SMTP_SERVER")
puerto = os.getenv("SMTP_PORT", "25")
if not servidor:
raise ValueError("Falta SMTP_SERVER")
return {
"servidor": servidor,
"puerto": int(puerto),
}
Escribe una prueba con servidor y puerto definidos, otra con puerto por defecto y otra sin servidor.
Una solución:
import pytest
from email_config import obtener_configuracion_email
def test_obtener_configuracion_email(monkeypatch):
monkeypatch.setenv("SMTP_SERVER", "smtp.test.local")
monkeypatch.setenv("SMTP_PORT", "2525")
configuracion = obtener_configuracion_email()
assert configuracion == {
"servidor": "smtp.test.local",
"puerto": 2525,
}
def test_obtener_configuracion_email_con_puerto_por_defecto(monkeypatch):
monkeypatch.setenv("SMTP_SERVER", "smtp.test.local")
monkeypatch.delenv("SMTP_PORT", raising=False)
configuracion = obtener_configuracion_email()
assert configuracion == {
"servidor": "smtp.test.local",
"puerto": 25,
}
def test_obtener_configuracion_email_sin_servidor(monkeypatch):
monkeypatch.delenv("SMTP_SERVER", raising=False)
with pytest.raises(ValueError):
obtener_configuracion_email()
Controlar configuración en pruebas evita resultados dependientes del entorno real. monkeypatch permite modificar variables de entorno, atributos y diccionarios de forma temporal, mientras que la inyección de configuración reduce la necesidad de parches.
En el próximo tema veremos cómo probar código que trabaja con archivos sin tocar el sistema real.