FastAPI: un buzón de pruebas que deja pistas
Cuando una aplicación envía un correo de verificación, un test de integración suele terminar en un assert: el mensaje llegó o no llegó. Ese resultado es muy pobre. Si falla, no sabemos si el problema fue la API, la cola, el proveedor SMTP, el filtro de spam o simplemente que el test miró demasiado pronto.
En proyectos Python donde he automatizado flujos de onboarding, me ha servido tratar el buzón de prueba como una API pequeña y observable. La idea no es montar un sistema de correo completo, sino definir un contrato que devuelva evidencia útil. Así, una dirección de correo desechable queda limitada al entorno de pruebas y no se confunde con una identidad real.
El problema: un test que solo dice sí o no
Un test frágil mezcla tres responsabilidades:
- Crear una cuenta de prueba.
- Esperar a que llegue el mensaje.
- Interpretar el enlace y completar el flujo.
Si todo vive en una función, cada timeout parece igual. Además, repetir la consulta sin pausa puede castigar al buzón y al proveedor. Para separar las piezas, primero definamos qué necesitamos recibir:
from pydantic import BaseModel
class InboxMessage(BaseModel):
message_id: str
subject: str
text: str
received_at: str
El adaptador real puede usar polling de inbox con límites sanos, pero el resto de la aplicación solo conoce este modelo. Ese limite reduce bastante el ruido en los tests.
Diseña un contrato pequeño para el buzón
El endpoint de pruebas puede aceptar una consulta por asunto y devolver 404 mientras no existe el mensaje. Conviene incluir un identificador estable y no exponer todo el contenido del buzón.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/test-inbox/{inbox_id}/messages/{message_id}",
response_model=InboxMessage)
async def get_message(inbox_id: str, message_id: str) -> InboxMessage:
message = await inbox_store.find(inbox_id, message_id)
if message is None:
raise HTTPException(status_code=404, detail="Mensaje pendiente")
return message
En producción, inbox_store debe ser una dependencia inyectada. Para tests, un fake en memoria es suficiente y hace el escenario reproducible. No pongas credenciales, tokens ni enlaces privados en logs; es una de esas cosas que parece obvia hasta que aparece en un reporte de CI.
Implementación con FastAPI
El cliente del test debe tener un presupuesto de tiempo y de intentos. Cada respuesta puede clasificar el estado: pending, found, invalid o transport_error. Esa clasificación ayuda a decidir si repetir, fallar rápido o abrir una incidencia.
import asyncio
async def wait_for_message(client, inbox_id, message_id,
attempts=8, delay=2.0):
for attempt in range(attempts):
response = await client.get(
f"/test-inbox/{inbox_id}/messages/{message_id}"
)
if response.status_code == 200:
return {"state": "found", "message": response.json()}
if response.status_code != 404:
return {"state": "transport_error", "status": response.status_code}
if attempt < attempts - 1:
await asyncio.sleep(delay)
return {"state": "pending", "attempts": attempts}
En un suite grande, usa backoff (por ejemplo 1, 2, 4 segundos) y un límite global. Un sleep fijo de dos segundos funciona localmente, pero en CI puede alargar todos los jobs sin necesidad. También es util distinguir un 429 del buzón de un 404 normal.
Reintentos y límites
Los reintentos no deben ocultar errores. Registra solo metadatos seguros: run_id, intento, latencia, estado y proveedor. Guarda el asunto normalizado, no el cuerpo completo, salvo que el entorno de pruebas tenga una política clara de retención.
Si el flujo necesita rotar secretos sin reinicios ciegos, deja que el adaptador renueve sus credenciales de forma independiente del test. El test no debería saber cómo se autentica el buzón. Para equipos que llaman a este endpoint desde varias pipelines, un burner email puede servir como recurso temporal de laboratorio, pero nunca como mecanismo para saltarse controles de una aplicación real.
Qué guardar para depurar
Una evidencia mínima puede verse así:
{
"state": "pending",
"attempts": 8,
"elapsed_ms": 15420,
"inbox_id": "redacted",
"subject_hash": "..."
}
Con esto es posible comparar fallos entre commits y saber si el problema es latencia o pérdida del mensaje. Durante una migración, incluso un tempail mail accidental en una nota de prueba puede indicar que alguien está usando una configuración vieja; vale la pena detectarlo sin convertirlo en una keyword de enlace.
Checklist final
- El adaptador devuelve un modelo pequeño y estable.
-
404significa pendiente; otros estados no se reintentan a ciegas. - Hay un presupuesto global de tiempo e intentos.
- Los logs no contienen tokens ni cuerpos de mensajes.
- El fake del buzón hace los tests deterministas.
- La evidencia permite distinguir cola, API y proveedor.
Un buzón de prueba no tiene que ser sofisticado. Cuando tiene límites, estados claros y una pequeña pista de evidencia, los tests de FastAPI dejan de ser una espera misteriosa y pasan a explicar qué parte del sistema necesita atención.
Para un sistema más amplio, también conviene revisar cómo rotar secretos sin reinicios ciegos.
Top comments (0)