DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: prueba emails sin romper tu entorno

Cuando un flujo de registro falla, mucha gente revisa solo si el proveedor "envio algo". En mi experiencia con FastAPI, eso casi nunca alcanza. El problema de verdad suele estar en la frontera entre API, cola, plantilla y entorno. Si usas datos reales o inboxes mezclados, depurar se vuelve lento y medio confuso.

Por eso me gusta separar la prueba funcional de email con una regla bien simple: cada ejecucion de staging recibe su propia identidad, su propia traza y su propia inbox temporal. Si necesitas generar correo desechable para un caso puntual, que sea un recurso de prueba y no una dependencia rara pegada al sistema.

El error comun: mezclar pruebas de email y entorno real

He visto equipos guardar direcciones manuales en variables compartidas, reutilizar la misma inbox por dias y luego preguntarse por que el test "a veces" pasa. Tambien aparecen notas como temp mailid en tickets o mensajes de QA, y eso ya te avisa que el proceso depende demasiado de memoria humana.

El otro error es mirar solo el correo recibido y no el camino completo:

  1. la API acepta la solicitud
  2. la cola crea el trabajo
  3. el worker renderiza la plantilla
  4. el proveedor intenta la entrega
  5. la inbox de prueba confirma el resultado

Si solo validas el paso 5, te pierdes casi todo lo util. Para eso me han servido articulos sobre revisar emails de reactivacion con contexto, porque muestran bien que un email aislado dice poco sin el evento que lo produjo.

Un patron simple para aislar inboxes en FastAPI

Lo que mejor me ha funcionado es crear un run_id por ejecucion y propagarlo desde la request hasta el envio. No hace falta una arquitectura enorme, de hecho algo pequeño suele durar mas.

from fastapi import FastAPI, BackgroundTasks
from uuid import uuid4

app = FastAPI()

def queue_signup_email(user_id: str, inbox: str, run_id: str) -> None:
    payload = {
        "user_id": user_id,
        "inbox": inbox,
        "run_id": run_id,
        "template": "signup_verify",
    }
    print(payload)

@app.post("/signup-test")
def signup_test(user_id: str, background_tasks: BackgroundTasks):
    run_id = str(uuid4())
    inbox = f"{run_id[:8]}@example.test"
    background_tasks.add_task(queue_signup_email, user_id, inbox, run_id)
    return {"run_id": run_id, "inbox": inbox}
Enter fullscreen mode Exit fullscreen mode

La idea no es el print, claro. La idea es que el mismo run_id viva en logs, eventos y revisiones. Asi sabes que inbox corresponde a que intento, y no terminas cazando un tempail mail viejo de otra corrida. Parece obvio, pero cuando no existe esa relacion, el debugging se pone feo muy rapido.

Codigo minimo para crear trazas utiles

Despues del run_id, yo intento guardar solo tres cosas que casi siempre alcanzan:

  • estado normalizado: queued, sent, failed
  • identificador del proveedor
  • marca de tiempo del intento

Con eso puedes contestar preguntas operativas sin exponer contenido sensible ni abrir cinco paneles. Si ademas tu worker registra una razon corta de fallo, ya tienes una base bastante decente.

Tambien conviene que la automatizacion revise el resultado desde dos angulos:

  1. la API devolvio el run_id
  2. la inbox asociada recibio el mensaje esperado

Ese segundo paso se puede conectar con una guia como probar emails de referidos en flujos reales, sobre todo si en tu producto hay varias plantillas y no quieres validar todo con una sola direccion reciclada.

Si tu suite crece, una mejora bastante util es persistir un manifiesto corto por ejecucion:

{
  "run_id": "b51d7c3e",
  "template": "signup_verify",
  "expected_subject": "Confirma tu cuenta",
  "inbox": "b51d7c3e@example.test",
  "status": "sent"
}
Enter fullscreen mode Exit fullscreen mode

No es glamoroso, pero funciona. Ese archivito evita muchas discusiones tontas sobre cual email estabamos mirando, y hace que soporte o QA puedan revisar fallos sin entrar al codigo cada vez.

Que validar antes de automatizar mas

Antes de meter mas scripts, yo revisaria esto:

  1. Cada entorno usa inboxes distintas o nombres claramente aislados.
  2. Las pruebas limpian sus artefactos despues de un tiempo razonable.
  3. El worker puede reintentarse sin duplicar evidencia confusa.
  4. Los logs no guardan cuerpos completos de emails por defecto.
  5. Existe una manera corta de unir request, job e inbox.

Si esas cinco piezas estan, la automatizacion mejora mucho. Si no estan, cualquier herramienta que agregues solo tapa el ruido por un rato. Es mejor resolver primero el contrato minimo entre API y entrega, aun si al principio se siente un poco boring.

Q&A

¿Necesito una inbox temporal para todas las pruebas?

No. Para unit tests y varias integraciones basta con mocks o con una salida a archivo. La inbox temporal sirve mas cuando quieres validar el mensaje final que veria una persona.

¿Conviene revisar el HTML completo del email en cada corrida?

Solo en pruebas bien elegidas. Hacerlo siempre suele volver la suite mas fragil de lo necesario. Yo prefiero revisar asunto, CTA principal y uno o dos fragmentos criticos.

¿Que cambia cuando trabajo con varios entornos?

Que la disciplina importa mas. Si staging, QA y demos comparten destino, tarde o temprano vas a leer el email correcto en el momento equivocado. Y ese tipo de fallo te roba tiempo, aunque paresca pequeño.

Top comments (0)