28. Pruebas de módulos que usan requests, tiempo o variables de entorno

28.1 Objetivo del tema

Algunas pruebas fallan no por un error del programa, sino porque dependen de internet, de una variable de entorno ausente o de la fecha actual. Esas dependencias hacen que una prueba sea lenta, frágil o distinta cada día.

En este tema probaremos un módulo que usa requests, variables de entorno y fechas. La clave será reemplazar esas dependencias durante la prueba.

Idea clave: una prueba unitaria no debe depender de red real, hora real ni configuración externa no controlada.

28.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-dependencias-externas-demo
cd pytest-dependencias-externas-demo

Instala las dependencias necesarias:

python -m pip install pytest requests

28.3 Crear el módulo a probar

Crea un archivo llamado clima.py:

import os
from datetime import date, timedelta

import requests


API_URL = "https://api.example.com/clima"


def obtener_api_key():
    api_key = os.getenv("WEATHER_API_KEY")
    if not api_key:
        raise RuntimeError("Falta WEATHER_API_KEY")
    return api_key


def obtener_temperatura(ciudad):
    api_key = obtener_api_key()
    respuesta = requests.get(
        API_URL,
        params={"q": ciudad, "key": api_key},
        timeout=5,
    )
    respuesta.raise_for_status()
    datos = respuesta.json()
    return datos["temperatura"]


def clasificar_temperatura(temperatura):
    if temperatura < 10:
        return "frio"
    if temperatura > 28:
        return "calor"
    return "templado"


def resumen_clima(ciudad):
    temperatura = obtener_temperatura(ciudad)
    return {
        "ciudad": ciudad,
        "temperatura": temperatura,
        "clasificacion": clasificar_temperatura(temperatura),
    }


def fecha_vencimiento(dias):
    return date.today() + timedelta(days=dias)

El módulo tiene tres tipos de dependencias externas: configuración, red y fecha actual.

28.4 Probar variable de entorno presente

Usamos monkeypatch.setenv para definir la clave solo durante la prueba:

from clima import obtener_api_key


def test_obtener_api_key(monkeypatch):
    monkeypatch.setenv("WEATHER_API_KEY", "clave-test")

    assert obtener_api_key() == "clave-test"

28.5 Probar variable de entorno faltante

También debemos probar el caso en que la variable no existe:

import pytest


def test_obtener_api_key_faltante(monkeypatch):
    monkeypatch.delenv("WEATHER_API_KEY", raising=False)

    with pytest.raises(RuntimeError):
        obtener_api_key()

28.6 Crear una respuesta falsa para requests

Para evitar una llamada real a internet, creamos una clase que se comporte como una respuesta mínima de requests:

class RespuestaFake:
    def __init__(self, datos, status_ok=True):
        self.datos = datos
        self.status_ok = status_ok

    def raise_for_status(self):
        if not self.status_ok:
            raise RuntimeError("Error HTTP")

    def json(self):
        return self.datos

Solo implementamos los métodos que el código bajo prueba necesita.

28.7 Reemplazar requests.get

Con monkeypatch.setattr reemplazamos requests.get dentro del módulo clima:

import clima


def test_obtener_temperatura(monkeypatch):
    monkeypatch.setenv("WEATHER_API_KEY", "clave-test")

    def get_falso(url, params, timeout):
        assert url == clima.API_URL
        assert params == {"q": "Cordoba", "key": "clave-test"}
        assert timeout == 5
        return RespuestaFake({"temperatura": 22})

    monkeypatch.setattr(clima.requests, "get", get_falso)

    assert clima.obtener_temperatura("Cordoba") == 22

La prueba valida el resultado y también los argumentos usados para llamar a la API.

28.8 Simular error HTTP

Si la API responde con error, raise_for_status debe lanzar una excepción:

def test_obtener_temperatura_con_error_http(monkeypatch):
    monkeypatch.setenv("WEATHER_API_KEY", "clave-test")

    def get_falso(url, params, timeout):
        return RespuestaFake({}, status_ok=False)

    monkeypatch.setattr(clima.requests, "get", get_falso)

    with pytest.raises(RuntimeError):
        clima.obtener_temperatura("Cordoba")

La prueba no necesita provocar un error real en internet.

28.9 Probar lógica separada de requests

La función clasificar_temperatura no depende de red ni entorno. Se puede probar directamente:

import pytest

from clima import clasificar_temperatura


@pytest.mark.parametrize("temperatura, esperado", [
    (5, "frio"),
    (22, "templado"),
    (32, "calor"),
])
def test_clasificar_temperatura(temperatura, esperado):
    assert clasificar_temperatura(temperatura) == esperado

Separar la lógica pura reduce la cantidad de mocks necesarios.

28.10 Reemplazar una función del mismo módulo

Para probar resumen_clima, podemos reemplazar obtener_temperatura:

def test_resumen_clima(monkeypatch):
    monkeypatch.setattr(clima, "obtener_temperatura", lambda ciudad: 31)

    resultado = clima.resumen_clima("Mendoza")

    assert resultado == {
        "ciudad": "Mendoza",
        "temperatura": 31,
        "clasificacion": "calor",
    }

Así probamos el armado del resumen sin depender de la API.

28.11 El problema de date.today

Una función que usa date.today() puede cambiar de resultado todos los días:

def fecha_vencimiento(dias):
    return date.today() + timedelta(days=dias)

Para probarla, necesitamos controlar cuál es la fecha actual.

28.12 Reemplazar date con una clase falsa

Podemos crear una clase que herede de date y redefine today:

from datetime import date


class FechaFake(date):
    @classmethod
    def today(cls):
        return cls(2026, 5, 9)

Luego reemplazamos clima.date:

def test_fecha_vencimiento(monkeypatch):
    monkeypatch.setattr(clima, "date", FechaFake)

    assert clima.fecha_vencimiento(10) == date(2026, 5, 19)

La prueba dará el mismo resultado cualquier día que se ejecute.

28.13 Mejor diseño: recibir la fecha por parámetro

Otra opción es diseñar la función para recibir la fecha base:

def fecha_vencimiento_desde(fecha_base, dias):
    return fecha_base + timedelta(days=dias)

Ese diseño es más simple de probar porque no necesita reemplazar date.today.

28.14 Archivo completo de pruebas

El archivo test_clima.py puede quedar así:

from datetime import date

import pytest

import clima
from clima import clasificar_temperatura, obtener_api_key


class RespuestaFake:
    def __init__(self, datos, status_ok=True):
        self.datos = datos
        self.status_ok = status_ok

    def raise_for_status(self):
        if not self.status_ok:
            raise RuntimeError("Error HTTP")

    def json(self):
        return self.datos


def test_obtener_api_key(monkeypatch):
    monkeypatch.setenv("WEATHER_API_KEY", "clave-test")

    assert obtener_api_key() == "clave-test"


def test_obtener_api_key_faltante(monkeypatch):
    monkeypatch.delenv("WEATHER_API_KEY", raising=False)

    with pytest.raises(RuntimeError):
        obtener_api_key()


def test_obtener_temperatura(monkeypatch):
    monkeypatch.setenv("WEATHER_API_KEY", "clave-test")

    def get_falso(url, params, timeout):
        assert url == clima.API_URL
        assert params == {"q": "Cordoba", "key": "clave-test"}
        assert timeout == 5
        return RespuestaFake({"temperatura": 22})

    monkeypatch.setattr(clima.requests, "get", get_falso)

    assert clima.obtener_temperatura("Cordoba") == 22


def test_obtener_temperatura_con_error_http(monkeypatch):
    monkeypatch.setenv("WEATHER_API_KEY", "clave-test")

    def get_falso(url, params, timeout):
        return RespuestaFake({}, status_ok=False)

    monkeypatch.setattr(clima.requests, "get", get_falso)

    with pytest.raises(RuntimeError):
        clima.obtener_temperatura("Cordoba")


@pytest.mark.parametrize("temperatura, esperado", [
    (5, "frio"),
    (22, "templado"),
    (32, "calor"),
])
def test_clasificar_temperatura(temperatura, esperado):
    assert clasificar_temperatura(temperatura) == esperado


def test_resumen_clima(monkeypatch):
    monkeypatch.setattr(clima, "obtener_temperatura", lambda ciudad: 31)

    resultado = clima.resumen_clima("Mendoza")

    assert resultado == {
        "ciudad": "Mendoza",
        "temperatura": 31,
        "clasificacion": "calor",
    }


class FechaFake(date):
    @classmethod
    def today(cls):
        return cls(2026, 5, 9)


def test_fecha_vencimiento(monkeypatch):
    monkeypatch.setattr(clima, "date", FechaFake)

    assert clima.fecha_vencimiento(10) == date(2026, 5, 19)

28.15 Ejecutar las pruebas

Desde la raíz del proyecto, ejecuta:

python -m pytest

La salida esperada será similar a:

collected 9 items

test_clima.py .........                                         [100%]

9 passed in 0.06s

28.16 Qué conviene aislar

  • Red: reemplaza llamadas a requests.get, requests.post u otros clientes HTTP.
  • Tiempo: controla date.today, datetime.now o recibe la fecha como parámetro.
  • Entorno: usa monkeypatch.setenv y monkeypatch.delenv.
  • Lógica pura: sepárala y pruébala sin mocks cuando sea posible.

28.17 Errores frecuentes

  • Hacer llamadas HTTP reales: la prueba puede fallar por red, credenciales o cambios externos.
  • Depender de la fecha actual: la prueba puede pasar hoy y fallar mañana.
  • Olvidar variables de entorno: otra máquina puede no tener la misma configuración.
  • Mockear demasiado: separa la lógica pura para probarla directamente.
  • No probar errores externos: una API puede devolver error, timeout o datos incompletos.

28.18 Comandos usados en este tema

mkdir pytest-dependencias-externas-demo
cd pytest-dependencias-externas-demo
python -m pip install pytest requests
python -m pytest
python -m pytest -v
python -m pytest test_clima.py::test_obtener_temperatura -v

28.19 Qué debes recordar de este tema

  • Las pruebas unitarias no deben depender de internet real.
  • monkeypatch permite reemplazar llamadas a requests.
  • Las variables de entorno deben prepararse dentro de la prueba.
  • El tiempo real debe controlarse para evitar pruebas inestables.
  • Separar lógica pura reduce la necesidad de mocks.
  • Los errores de dependencias externas también deben probarse.

28.20 Conclusión

En este tema probamos código que depende de requests, variables de entorno y fechas. Reemplazamos cada dependencia para que las pruebas sean rápidas, repetibles y aisladas.

En el próximo tema veremos marcadores, skip, xfail y selección de pruebas con pytest.