DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: un buzón de pruebas que deja pistas

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:

  1. Crear una cuenta de prueba.
  2. Esperar a que llegue el mensaje.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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": "..."
}
Enter fullscreen mode Exit fullscreen mode

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.
  • 404 significa 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)