TestClient ejercita rutas FastAPI por HTTP. Las dependencias externas pueden sustituirse mediante app.dependency_overrides, siempre que la limpieza restaure el estado global.
from fastapi.testclient import TestClient
from app.main import app, usuario_actual
def usuario_falso() -> dict[str, object]:
return {"id": 7, "admin": False}
def test_perfil() -> None:
app.dependency_overrides[usuario_actual] = usuario_falso
try:
with TestClient(app) as client:
response = client.get("/perfil")
assert response.status_code == 200
assert response.json()["id"] == 7
finally:
app.dependency_overrides.clear()
El context manager ejecuta eventos de ciclo de vida. En suites grandes, mueve override y limpieza a una fixture. Comprueba estado, schema y efectos relevantes.
Usa base aislada, transacción descartable o repositorio fake según el nivel. Consulta FastAPI, fixtures pytest y pytest-asyncio.
La documentación oficial de pruebas FastAPI, consultada el 22 de julio de 2026, explica TestClient y overrides. Mantén pruebas unitarias rápidas y una capa menor de integración real.
Qué debe demostrar una prueba de ruta
Una prueba útil verifica el contrato público, no la reproducción de la implementación del endpoint. Envía el mismo método, ruta, headers, query y cuerpo JSON que enviaría un cliente. Después inspecciona el status, los headers de la respuesta y el cuerpo decodificado. En un endpoint de creación, también confirma el efecto observable de persistencia. En una regla de autorización, prueba una solicitud anónima, una autenticada sin permiso y una autorizada.
Evita afirmar todos los campos cuando solo dos pertenecen al comportamiento bajo prueba. Una comparación literal completa se vuelve ruidosa cuando el schema gana un campo opcional irrelevante. Por otro lado, comprobar solo status_code == 200 puede ocultar un payload roto. Elige aserciones que describan el contrato: identificadores con el valor esperado, secretos ausentes, errores de validación apuntando a la entrada correcta y metadatos de paginación coherentes.
FastAPI valida la entrada antes de llamar al endpoint. Incluye JSON malformado, valores obligatorios ausentes, límites y tipos incorrectos. No fijes la respuesta completa de validación salvo que el formato exacto forme parte del contrato de la API. Suele ser más seguro encontrar el ítem relevante en response.json()["detail"] y afirmar la ubicación y la categoría del error.
Construye fixtures reutilizables con pytest
Crea la aplicación y el client en fixtures cuando el setup sea compartido. La fixture debe ser dueña de todo lo que crea y liberarlo después del yield. Esta regla importa porque dependency_overrides es un diccionario mutable en el objeto de la aplicación.
import pytest
from fastapi.testclient import TestClient
from app.main import create_app, obtener_repositorio
@pytest.fixture
def repositorio_falso():
return RepositorioUsuarioMemoria()
@pytest.fixture
def client(repositorio_falso):
app = create_app()
app.dependency_overrides[obtener_repositorio] = lambda: repositorio_falso
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
Una factory de aplicación da a cada prueba un app nuevo y reduce el acoplamiento accidental. Si el proyecto exporta un app global, guarda el override anterior y restáuralo en lugar de limpiar overrides instalados por otra fixture. Mantén el alcance en function hasta que las mediciones justifiquen un estado más amplio. Los fakes mutables con alcance de sesión suelen hacer que el resultado dependa del orden de ejecución.
Usa la forma with TestClient(app) cuando la aplicación tenga lifespan. Entrar en el contexto inicia recursos como pools de conexión; salir ejecuta el apagado. Instanciar el client sin el context manager solo es aceptable cuando el comportamiento no depende de esos eventos.
Sustituye dependencias en el límite correcto
Sustituye el callable pasado a Depends, no un helper distinto que se llame por casualidad dentro de él. La clave del diccionario es el objeto de la función original. La sustitución puede ser síncrona o asíncrona según el caso y debe devolver un valor compatible.
La autenticación es un buen límite. Devuelve un usuario de dominio pequeño con permisos explícitos y prueba la decodificación del token por separado. Un repositorio o servicio también es un límite útil porque evita red y base de datos, pero mantiene el parsing, la inyección, la lógica del endpoint y la serialización.
No sustituyas todo. Si la validación, la serialización o el mapeo de excepciones es el tema, mantén esos componentes reales. Una prueba en la que todos los colaboradores son mocks puede pasar mientras la ruta montada es inutilizable. Reserva pruebas directas de función para reglas de dominio complejas y pruebas de ruta para la integración HTTP.
Prueba excepciones, headers y seguridad
Para fallos conocidos, afirma el status previsto y un mensaje público seguro. Un registro ausente puede producir 404; un recurso duplicado, 409. No deben aparecer detalles internos, SQL, tokens ni stack traces. Si la API instala handlers de excepción personalizados, ejercítalos a través de una ruta.
Las pruebas de autenticación deben incluir header ausente, esquema inválido, credencial caducada y alcance insuficiente cuando corresponda. No pongas credenciales reales en el código ni en la salida capturada. Construye tokens sintéticos con claves solo de prueba, o sustituye la dependencia de identidad ya verificada.
Para cookies, redirects, descargas y respuestas en streaming, inspecciona el comportamiento HTTP correspondiente. TestClient sigue redirects por defecto, lo que puede ocultar el 307 o 302 original; desactiva el follow cuando el redirect en sí sea el contrato.
Pruebas async con HTTPX
TestClient es síncrono aunque la ruta use async def. Es conveniente para la mayoría de las pruebas de endpoint. Usa una prueba asíncrona cuando la propia prueba deba esperar un repositorio, cola o sesión async. La documentación de FastAPI describe HTTPX con ASGITransport para ese caso.
import pytest
from httpx import ASGITransport, AsyncClient
@pytest.mark.anyio
async def test_health(app) -> None:
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
) as client:
response = await client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
El transport envía solicitudes directamente a la aplicación ASGI y no abre un puerto real. La gestión del lifespan puede exigir una fixture explícita según la configuración de HTTPX. Mantén el plugin del runner y la política del event loop coherentes.
Aislamiento de base y niveles de prueba
Elige el realismo de la base a propósito. Un repositorio en memoria es rápido, pero no revela sintaxis SQL, constraints, comportamiento de transacción ni diferencias de driver. SQLite no sustituye fielmente cada recurso de PostgreSQL. Mantén una suite menor de integración contra el engine usado en producción, normalmente con una base descartable o un contenedor.
Las fixtures basadas en rollback son rápidas, pero el código que abre conexiones independientes o hace commit propio puede escapar del rollback externo. Confirma los límites reales de conexión y transacción. Schemas únicos o bases recreadas dan un aislamiento más fuerte con mayor coste de setup.
Organiza la suite en muchas pruebas unitarias enfocadas, pruebas de contrato de ruta con dependencias controladas y menos pruebas de integración de infraestructura. Añade extremo a extremo solo para recorridos críticos. Esa distribución da feedback rápido sin pretender que los fakes demuestren compatibilidad con la base.
Mantén la suite determinista
Congela el tiempo mediante una dependencia de reloj en lugar de parchear muchas llamadas de biblioteca. Genera identificadores estables o afirma el formato, no un valor aleatorio exacto. Desactiva el acceso de red de salida por defecto para que un override omitido falle de inmediato. Para tareas en segundo plano, verifica la intención registrada sin contactar al proveedor real.
La ejecución en paralelo expone estado compartido oculto. Evita repositorios mutables a nivel de módulo, filas reutilizadas, nombres temporales fijos y overrides globales. Una prueba que falla debe ser reproducible sola y en orden aleatorio.
Antes del merge, ejecuta la suite de rutas con warnings visibles. Trata las deprecaciones de FastAPI, Starlette, Pydantic y HTTPX como señales de mantenimiento. Las buenas pruebas de API documentan el comportamiento, detectan upgrades incompatibles y siguen siendo lo bastante legibles para que un revisor entienda el contrato prometido sin abrir la implementación del endpoint.