18. Manejo de variables de entorno y configuración por ambiente

18.1 Objetivo del tema

Muchas suites automatizadas necesitan comportarse distinto según el ambiente: local, pruebas, staging o producción. Para eso se usan variables de entorno y archivos de configuración.

En este tema veremos cómo leer variables de entorno, cargar valores desde .env, convertir tipos y probar configuración con monkeypatch sin depender del ambiente real de la computadora.

Objetivo práctico: automatizar pruebas de configuración sin depender de variables reales del sistema operativo.

18.2 Qué es una variable de entorno

Una variable de entorno es un valor disponible para un proceso. En Python se puede leer con os.getenv.

import os


ambiente = os.getenv("APP_ENV", "local")

El segundo argumento es el valor por defecto si la variable no existe.

18.3 Crear un archivo .env

En la raíz del proyecto, crea o ajusta el archivo .env:

APP_ENV=local
TEST_TIMEOUT=5
REPORTS_DIR=reports
FEATURE_CUPONES=true

python-dotenv permite cargar esos valores como variables de entorno durante la ejecución local.

18.4 Crear un módulo de configuración

Crea o actualiza app/config.py:

import os

from dotenv import load_dotenv


load_dotenv()


def obtener_ambiente():
    return os.getenv("APP_ENV", "local")


def obtener_timeout():
    return int(os.getenv("TEST_TIMEOUT", "5"))


def obtener_carpeta_reportes():
    return os.getenv("REPORTS_DIR", "reports")


def cupones_habilitados():
    return os.getenv("FEATURE_CUPONES", "false").lower() == "true"

Este módulo centraliza la lectura de configuración. Las pruebas y scripts no deberían leer todas las variables por su cuenta.

18.5 Probar valores cargados desde .env

Crea tests/test_config_ambiente.py:

from app.config import (
    cupones_habilitados,
    obtener_ambiente,
    obtener_carpeta_reportes,
    obtener_timeout,
)


def test_configuracion_lee_ambiente_local():
    assert obtener_ambiente() == "local"


def test_configuracion_lee_timeout_como_entero():
    assert obtener_timeout() == 5


def test_configuracion_lee_carpeta_reportes():
    assert obtener_carpeta_reportes() == "reports"


def test_configuracion_lee_feature_cupones():
    assert cupones_habilitados() is True

Ejecuta:

python -m pytest tests/test_config_ambiente.py

18.6 Problema de depender del .env real

Las pruebas anteriores dependen de que el archivo .env tenga valores específicos. Si otro alumno cambia el archivo, las pruebas pueden fallar aunque el código esté bien.

Para probar configuración de forma controlada, conviene usar monkeypatch y definir variables dentro de cada prueba.

18.7 Usar monkeypatch.setenv

monkeypatch.setenv permite definir una variable de entorno durante una prueba:

from app.config import obtener_ambiente


def test_obtener_ambiente_desde_variable(monkeypatch):
    monkeypatch.setenv("APP_ENV", "testing")

    assert obtener_ambiente() == "testing"

La variable se restaura automáticamente al terminar la prueba.

18.8 Usar monkeypatch.delenv

Para probar valores por defecto, podemos eliminar una variable durante la prueba:

from app.config import obtener_ambiente


def test_obtener_ambiente_sin_variable_devuelve_local(monkeypatch):
    monkeypatch.delenv("APP_ENV", raising=False)

    assert obtener_ambiente() == "local"

raising=False evita error si la variable no existe.

18.9 Probar conversión de enteros

Las variables de entorno siempre llegan como texto. Si necesitamos números, hay que convertirlos.

from app.config import obtener_timeout


def test_obtener_timeout_convierte_variable_a_entero(monkeypatch):
    monkeypatch.setenv("TEST_TIMEOUT", "10")

    assert obtener_timeout() == 10

La prueba verifica que el módulo de configuración convierta correctamente el valor.

18.10 Probar booleanos

Para valores booleanos, conviene definir reglas claras. En nuestro caso, solo true habilita la funcionalidad.

from app.config import cupones_habilitados


def test_cupones_habilitados_con_true_devuelve_true(monkeypatch):
    monkeypatch.setenv("FEATURE_CUPONES", "true")

    assert cupones_habilitados() is True


def test_cupones_habilitados_con_false_devuelve_false(monkeypatch):
    monkeypatch.setenv("FEATURE_CUPONES", "false")

    assert cupones_habilitados() is False

18.11 Probar distintos ambientes

Podemos agregar una función para saber si estamos en ambiente local:

def es_ambiente_local():
    return obtener_ambiente() == "local"

Prueba:

from app.config import es_ambiente_local


def test_es_ambiente_local_con_local_devuelve_true(monkeypatch):
    monkeypatch.setenv("APP_ENV", "local")

    assert es_ambiente_local() is True


def test_es_ambiente_local_con_testing_devuelve_false(monkeypatch):
    monkeypatch.setenv("APP_ENV", "testing")

    assert es_ambiente_local() is False

18.12 Parametrizar ambientes

La configuración por ambiente se presta bien para parametrización:

import pytest

from app.config import es_ambiente_local


@pytest.mark.parametrize("ambiente, esperado", [
    ("local", True),
    ("testing", False),
    ("staging", False),
])
def test_es_ambiente_local_devuelve_resultado_esperado(monkeypatch, ambiente, esperado):
    monkeypatch.setenv("APP_ENV", ambiente)

    assert es_ambiente_local() is esperado

18.13 Evitar secretos reales en pruebas

No guardes contraseñas, tokens ni claves reales en archivos de prueba. Si una función necesita una clave, usa valores falsos controlados.

def test_api_key_se_lee_desde_variable(monkeypatch):
    monkeypatch.setenv("API_KEY", "clave-falsa-para-pruebas")

    assert obtener_api_key() == "clave-falsa-para-pruebas"

El objetivo es verificar la lectura de configuración, no usar credenciales reales.

18.14 Crear una función para API_KEY

Agrega en app/config.py:

def obtener_api_key():
    return os.getenv("API_KEY", "")

Y prueba:

from app.config import obtener_api_key


def test_obtener_api_key_devuelve_variable_configurada(monkeypatch):
    monkeypatch.setenv("API_KEY", "clave-falsa")

    assert obtener_api_key() == "clave-falsa"

18.15 Configuración por ambiente

Podemos devolver una configuración distinta según APP_ENV:

def obtener_configuracion():
    ambiente = obtener_ambiente()
    configuraciones = {
        "local": {"debug": True, "base_url": "http://localhost:8000"},
        "testing": {"debug": False, "base_url": "http://testing.local"},
        "staging": {"debug": False, "base_url": "https://staging.example.com"},
    }
    return configuraciones.get(ambiente, configuraciones["local"])

Esta función evita que el resto del proyecto conozca los detalles de cada ambiente.

18.16 Probar configuración por ambiente

Prueba la función anterior:

from app.config import obtener_configuracion


def test_obtener_configuracion_para_testing(monkeypatch):
    monkeypatch.setenv("APP_ENV", "testing")

    config = obtener_configuracion()

    assert config["debug"] is False
    assert config["base_url"] == "http://testing.local"

18.17 Usar fixtures para ambientes

Si varias pruebas necesitan el mismo ambiente, puedes crear una fixture:

import pytest


@pytest.fixture
def ambiente_testing(monkeypatch):
    monkeypatch.setenv("APP_ENV", "testing")

Uso:

def test_obtener_configuracion_con_fixture_testing(ambiente_testing):
    config = obtener_configuracion()

    assert config["base_url"] == "http://testing.local"

18.18 Cuándo usar .env y cuándo monkeypatch

Usa .env para ejecutar el proyecto localmente con una configuración cómoda. Usa monkeypatch para pruebas automatizadas que necesitan controlar variables sin depender del entorno real.

  • .env: configuración local del proyecto.
  • monkeypatch.setenv: definir valores específicos durante una prueba.
  • monkeypatch.delenv: probar comportamiento cuando falta una variable.

18.19 Problemas frecuentes

  • La prueba pasa en una computadora y falla en otra: probablemente depende de variables reales del sistema.
  • Un número llega como texto: convierte con int o float.
  • Un booleano no funciona: define claramente qué textos equivalen a verdadero.
  • El .env no se carga: revisa que load_dotenv() se ejecute y que el archivo esté en la raíz esperada.
  • Se filtraron secretos: usa valores falsos en pruebas y excluye .env del repositorio.

18.20 Ejercicio práctico

Agrega en app/config.py una función obtener_max_reintentos. Debe leer MAX_REINTENTOS y devolver un entero. Si la variable no existe, debe devolver 3.

Luego crea pruebas para:

  • Variable definida con valor 5.
  • Variable ausente, usando el valor por defecto 3.
  • Variable definida con valor 0.

18.21 Solución propuesta

Función:

def obtener_max_reintentos():
    return int(os.getenv("MAX_REINTENTOS", "3"))

Pruebas:

from app.config import obtener_max_reintentos


def test_obtener_max_reintentos_con_variable_definida(monkeypatch):
    monkeypatch.setenv("MAX_REINTENTOS", "5")

    assert obtener_max_reintentos() == 5


def test_obtener_max_reintentos_sin_variable_devuelve_tres(monkeypatch):
    monkeypatch.delenv("MAX_REINTENTOS", raising=False)

    assert obtener_max_reintentos() == 3


def test_obtener_max_reintentos_con_cero_devuelve_cero(monkeypatch):
    monkeypatch.setenv("MAX_REINTENTOS", "0")

    assert obtener_max_reintentos() == 0

Ejecuta:

python -m pytest tests/test_config_ambiente.py

18.22 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Centralizas la configuración en app/config.py.
  • Usas valores por defecto cuando una variable puede faltar.
  • Conviertes tipos explícitamente.
  • Usas monkeypatch.setenv para definir variables en pruebas.
  • Usas monkeypatch.delenv para probar variables ausentes.
  • No usas secretos reales en pruebas.
  • La suite se ejecuta correctamente con python -m pytest.

18.23 Conclusión

En este tema trabajamos con variables de entorno y configuración por ambiente. Aprendimos a evitar pruebas dependientes del entorno real usando monkeypatch para controlar cada caso.

En el próximo tema automatizaremos verificaciones de salida, logs y mensajes de error, otro aspecto clave para diagnosticar fallas en suites automatizadas.