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.
Crea un proyecto nuevo:
mkdir pytest-dependencias-externas-demo
cd pytest-dependencias-externas-demo
Instala las dependencias necesarias:
python -m pip install pytest requests
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.
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"
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()
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.
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.
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.
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.
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.
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.
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.
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.
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)
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
requests.get, requests.post u otros clientes HTTP.date.today, datetime.now o recibe la fecha como parámetro.monkeypatch.setenv y monkeypatch.delenv.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
monkeypatch permite reemplazar llamadas a requests.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.