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.
 
