Estrategias de pruebas en FastAPI para elevar la calidad: pytest, TestClient/httpx, sustitución de dependencias, rollbacks de BD, mocks, pruebas de contrato y pruebas de carga
Resumen (pirámide invertida)
- Prepare pruebas en capas—unitarias, API, integración, contrato y carga—y cúbralas paso a paso.
- Diseñe fixtures con
pytesty useTestClientvs.httpx.AsyncClientsegún corresponda. - Aproveche la inyección de dependencias (
Depends) y cambie dependencias víaapp.dependency_overridesde forma sucinta. - Mantenga la BD limpia con rollbacks de transacciones o un engine dedicado; simule APIs externas con herramientas como
respx. - Valide OpenAPI con pruebas de contrato y añada
pytest-benchmarkolocustpara construir confianza operativa.
A quién beneficia
- Alumno A (tesis / solo dev)
Quiere un starter kit pequeño de pruebas. Primero, automatizar dos rutas por API: éxito y fallo. - Equipo pequeño B (taller de 3 personas)
Cambios frecuentes de especificación. Use overrides de dependencias, rollbacks de BD y mocks de APIs externas para localizar roturas rápido. - Equipo SaaS C (startup)
Temor a incidentes en producción. Quiere pruebas de contrato compatibles con OpenAPI y benchmarks / carga en CI.
1. El mapa de pruebas (empiece con la visión completa)
- Pruebas unitarias: verifican lógica de funciones/servicios rápidamente.
- Pruebas de API: verifican entrada/salida para
GET/POST(éxito/fallo/límites). - Pruebas de integración: verifican comportamiento usando recursos reales (BD/APIs externas).
- Pruebas de contrato: verifican conformidad con OpenAPI y reproducibilidad de ejemplos.
- Carga y benchmarks: entienden tendencias de latencia/rendimiento.
Puntos de decisión
- Empiece con unitarias y API. Después, aísle efectos externos con overrides de dependencias. Finalmente añada integración y carga.
2. Configuración mínima (bases de pytest)
Ejemplo pytest.ini:
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
addopts = -q
Dependencias de ejemplo (requirements):
pytest
httpx
pytest-asyncio
respx # mock para httpx
pytest-benchmark # opcional: benchmarks
Estructura de proyecto ejemplo:
app/
main.py
routers/
articles.py
core/
settings.py
security.py
db/
session.py
tests/
conftest.py
test_articles_api.py
test_services_unit.py
3. Levantar la app en pruebas (sync vs. async)
- Endpoints mayormente sync:
fastapi.testclient.TestClientes conveniente. - Para endpoints async/WebSocket o muchos
async def: prefierahttpx.AsyncClient.
API de muestra (SUT):
# app/routers/articles.py
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
router = APIRouter(prefix="/articles", tags=["articles"])
class Article(BaseModel):
id: int
title: str
FAKE = {1: {"id": 1, "title": "hello"}}
@router.get("/{aid}", response_model=Article)
def get_article(aid: int):
a = FAKE.get(aid)
if not a:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not found")
return a
main.py:
# app/main.py
from fastapi import FastAPI
from app.routers import articles
app = FastAPI(title="Testable API")
app.include_router(articles.router)
4. Comprobaciones rápidas sync con TestClient
# tests/test_articles_api.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_get_article_ok():
r = client.get("/articles/1")
assert r.status_code == 200
body = r.json()
assert body["id"] == 1
assert "title" in body
def test_get_article_404():
r = client.get("/articles/999")
assert r.status_code == 404
assert r.json()["detail"] == "not found"
Puntos de decisión
- Escriba siempre un par mínimo de éxito + fallo. Añada casos límite (mín/máx/inexistente) para endurecer cobertura.
5. Pruebas async con httpx.AsyncClient
# tests/test_async_api.py
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_get_article_async():
async with AsyncClient(app=app, base_url="http://test") as ac:
res = await ac.get("/articles/1")
assert res.status_code == 200
Puntos de decisión
AsyncClient(app=app)corre en modo ASGI embebido—sin servidor externo, bucle interno rápido.
6. Overrides de dependencias (el poder de Depends)
- Dependencias como sesiones de BD o auth pueden intercambiarse con
app.dependency_overrides. - Esto le permite aislar comportamiento de API de auth/permisos/BD.
Ejemplo: sobreescribir una dependencia de auth
# app/core/security.py (supuesto)
from fastapi import Depends, HTTPException, status
def get_current_user():
# En realidad verificaría JWT, etc.
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="unauthorized")
En las pruebas:
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.core import security
@pytest.fixture(autouse=True)
def override_auth():
def fake_user():
return {"sub": "test-user", "role": "admin"}
app.dependency_overrides[security.get_current_user] = fake_user
yield
app.dependency_overrides.clear()
@pytest.fixture
def client():
return TestClient(app)
Puntos de decisión
- Limpie siempre los overrides al final del test para evitar fugas entre pruebas.
7. Pruebas con BD: evite contaminación con transacciones
Tres estrategias:
- BD en memoria (aprendizaje / ultra-rápido)
- BD de pruebas + rollbacks de transacción (más cercano a prod)
- Ejecutar migraciones primero (Alembic)
Ejemplo SQLAlchemy 2.x:
# tests/db_utils.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db.session import Base # Declarative Base
from app.db.session import get_db # dependency function
@pytest.fixture(scope="session")
def engine():
# Para estar más cerca de producción, use una BD de pruebas (aquí sqlite en memoria)
eng = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
Base.metadata.create_all(eng)
return eng
@pytest.fixture
def db_session(engine):
connection = engine.connect()
trans = connection.begin()
Session = sessionmaker(bind=connection, autoflush=False, autocommit=False)
session = Session()
yield session
session.close()
trans.rollback()
connection.close()
@pytest.fixture(autouse=True)
def override_db(db_session):
from app.main import app
def _get_db():
try:
yield db_session
finally:
pass
app.dependency_overrides[get_db] = _get_db
yield
app.dependency_overrides.clear()
Puntos de decisión
- Inicie una transacción por test → rollback en teardown. Rápido y reproducible.
8. Mocking de HTTP externo (httpx × respx)
Corte la red para pruebas que dependen de APIs externas.
# tests/test_external_calls.py
import respx
from httpx import Response
import pytest
from httpx import AsyncClient
from app.main import app
@respx.mock
@pytest.mark.asyncio
async def test_external_call():
respx.get("https://api.example.com/users/1").mock(
return_value=Response(200, json={"id": 1, "name": "Alice"})
)
async with AsyncClient(app=app, base_url="http://test") as ac:
r = await ac.get("/proxy/users/1") # endpoint proxy de la app (supuesto)
assert r.status_code == 200
assert r.json()["name"] == "Alice"
Puntos de decisión
- Simule rutas normales y de fallo (5xx/timeouts) para verificar retries/fallbacks.
9. Pruebas de AuthN/AuthZ (scopes y acceso)
- API de login: éxitos y fallos (email incorrecto, contraseña incorrecta, usuario bloqueado).
- Rutas protegidas: sin token → 401, scope insuficiente → 403, scope suficiente → 200.
Ejemplo:
def test_protected_401(client):
res = client.get("/protected")
assert res.status_code == 401
def test_protected_403(client, monkeypatch):
from app.core import security
def low_scope_user():
return {"sub": "u", "scopes": {"articles:read"}}
app = security # alias
from app.main import app as fastapi_app
fastapi_app.dependency_overrides[security.get_current_user] = low_scope_user
res = client.post("/articles") # asuma que requiere *write scope*
assert res.status_code == 403
Puntos de decisión
- Mantenga los scopes requeridos mínimos por ruta. Las pruebas deben aclarar diferencias de scope vía overrides.
10. Validación y exception handlers
- Fije la forma de errores de Pydantic y respuestas de error estandarizadas.
- Distinga
422 Unprocessable Entity(entrada inválida) de errores de negocio personalizados.
def test_validation_422(client):
res = client.post("/users", json={"email": "not-an-email"})
assert res.status_code == 422
body = res.json()
assert "detail" in body
Puntos de decisión
- Si cambian las formas de error, los clientes se rompen. Las snapshot tests ayudan a fijarlas.
11. Pruebas de contrato contra OpenAPI
- Integridad del esquema: requeridos/typing/valores de enum.
- Reproducibilidad de ejemplos: ¿las peticiones de ejemplo se comportan como se documenta?
Chequeo simple:
# tests/test_openapi_contract.py
from app.main import app
def test_openapi_basic():
spec = app.openapi()
assert spec["info"]["title"] == "Testable API"
paths = spec["paths"]
assert "/articles/{aid}" in paths
Enfoques más potentes incluyen herramientas que generan pruebas desde el esquema (ver referencias).
12. Pruebas basadas en propiedades (property-based)
Use hypothesis para generar automáticamente valores cercanos a límites.
Ejemplo (unitaria):
# tests/test_services_unit.py
from hypothesis import given, strategies as st
def safe_div(a: int, b: int) -> float | None:
if b == 0:
return None
return a / b
@given(st.integers(), st.integers())
def test_safe_division(a, b):
r = safe_div(a, b)
if b == 0:
assert r is None
else:
assert isinstance(r, float)
Puntos de decisión
- Mejor para capas de servicio con lógica pesada en vez de la capa de API.
13. Benchmarks y pruebas de carga
Benchmarks (unitarios/API micro):
# tests/test_bench.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
@pytest.mark.benchmark(min_rounds=5)
def test_get_article_bench(benchmark):
def do():
assert client.get("/articles/1").status_code == 200
benchmark(do)
Pruebas de carga (proceso aparte):
- Use
locustok6para describir escenarios (auth → list → detail → create). - Enfoque métricas en distribución de latencia, tasa de error y throughput (métrica RED).
Puntos de decisión
- En CI, ejecute benchmarks cortos para detectar regresiones; ejecute carga en staging.
14. Pruebas de WebSocket/SSE
- WebSocket: use
websockets/starlette.testclientpara conectar y verificar enviar/recibir. - SSE: conecte con
httpxy parsee eventos del flujo.
Ejemplo WebSocket:
# tests/test_ws.py
from starlette.testclient import TestClient
from app.main import app
def test_ws_echo():
with TestClient(app).websocket_connect("/realtime/ws?room=test") as ws:
ws.send_json({"msg": "hi"})
data = ws.receive_json()
assert "echo" in data
Puntos de decisión
- Simule heartbeats y desconexiones anómalas; detecte conexiones filtradas.
15. Ejecución en CI y artefactos
- Ejecución de tests:
pytest -q - Cobertura: visualice módulos clave con
coverage. - Artefactos: almacene logs de fallos, JSON de OpenAPI, capturas (si aplica).
- Paralelismo:
pytest -n auto(pytest-xdist) para acelerar.
Outline de GitHub Actions:
name: test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install -r requirements.txt
- run: pip install pytest pytest-asyncio respx pytest-benchmark
- run: pytest -q
Puntos de decisión
- Las pruebas deben correr en cada PR. Considere exportar OpenAPI y generar SDKs aquí también.
16. Problemas comunes y remedios
| Síntoma | Causa | Remedio |
|---|---|---|
| Pruebas lentas | Golpean BD/APIs reales | Overrides/mocks, rollbacks de transacción, ejecuciones en paralelo |
| Pruebas inestables | Dependencias de tiempo/aleatoriedad/orden | Seeds fijos, inyección de tiempo, aislamiento de E/S, pytest-randomly |
| Deriva de especificación | Docs no actualizadas | Pruebas de contrato OpenAPI, validar ejemplos |
| Fallos solo en prod | Diferencias de config/red | .env separado, inyecte settings como dependencias, timeouts cortos |
| Fugas de limpieza | Sesiones sin cerrar / overrides residuales | Fixtures con yield, app.dependency_overrides.clear() |
17. Hoja de ruta de adopción
- Para APIs clave, escriba dos pruebas con
TestClient: éxito y fallo. - Introduzca overrides de dependencias para desacoplar auth/BD y ganar velocidad.
- Mantenga la BD limpia con rollbacks; simule externos con
respx. - Añada pruebas de contrato OpenAPI para detectar roturas de esquema/ejemplos temprano.
- Gradualmente añada benchmarks y carga; integre en CI.
Referencias
- FastAPI
- pytest
- httpx / mocks
- OpenAPI / contratos
- Pruebas de carga
Conclusiones
- Separe sus capas de prueba y primero asegure éxito/fallo de las APIs con pruebas pequeñas.
- Use overrides de
Depends, rollbacks de BD y mocks externos para hacer pruebas rápidas y resilientes. - Ponga pruebas de contrato OpenAPI y benchmarks ligeros en CI para detectar regresiones de especificación y rendimiento temprano.
- El objetivo es el cambio seguro. Escriba dos pruebas hoy y añada overrides de dependencias después.
