20. Controlar variables de entorno y configuración en las pruebas

20.1 Objetivo del tema

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.

Objetivo práctico: evitar que las pruebas dependan de la configuración real de la máquina o del entorno donde se ejecutan.

20.2 Problema de depender del entorno real

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.

20.3 Usar monkeypatch.setenv

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.

20.4 Usar monkeypatch.delenv

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.

20.5 Convertir valores de entorno

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.

20.6 Manejar configuración inválida

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()

20.7 Booleanos desde entorno

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

20.8 Evitar leer configuración al importar

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.

20.9 Preferir lectura en función

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.

Leer configuración al importar puede hacer que las pruebas dependan del orden de importación.

20.10 Parchear constantes de módulo

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

20.11 Configuración como diccionario

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

20.12 Inyectar configuración

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.

20.13 Configuración con dataclass

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}

20.14 Fixtures para configuración

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.

20.15 Secretos y claves

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"

20.16 Probar ausencia de configuración obligatoria

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.

20.17 Buenas prácticas

  • No dependas de variables reales de la máquina del desarrollador.
  • Usa monkeypatch.setenv y delenv para controlar el entorno.
  • Evita leer configuración al importar módulos si eso dificulta las pruebas.
  • Prefiere inyectar configuración cuando sea razonable.
  • No uses secretos reales en pruebas automatizadas.

20.18 Ejercicio práctico

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.

20.19 Solución posible del ejercicio

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()

20.20 Conclusión

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.