En un flujo de signup, el email suele parecer una tarea secundaria: crear la cuenta, enviar un enlace y esperar que aparezca en una bandeja de prueba. El problema llega cuando el test pregunta cada pocos segundos si ya hay un mensaje nuevo. Ese polling funciona al principio, pero pronto se vuelve lento, frágil y dificil de explicar.
He visto el mismo fallo en proyectos con Python: el endpoint responde bien, el worker envia el mensaje, pero la prueba mira la bandeja demasiado pronto. Luego se añade un sleep(3), despues otro reintento y, finalmente, un test que pasa en local pero falla en CI. No es que el correo sea misterioso. Falta un contrato claro entre el evento que produce el backend y la evidencia que consume la prueba.
El problema de esperar una bandeja
Un polling ciego mezcla varias preguntas distintas:
- ¿Se creó la cuenta?
- ¿Se generó el evento de verificación?
- ¿El proveedor aceptó el mensaje?
- ¿La bandeja de prueba lo recibió?
- ¿El test está leyendo el mensaje correcto?
Cuando todas se convierten en “espera hasta que aparezca un email”, cualquier retraso parece un fallo de aplicación. Ademas, si se reutiliza la misma bandeja entre tests paralelos, un mensaje viejo puede hacer que el caso equivocado pase.
Una solución sencilla es tratar el email como un evento observable. El endpoint no necesita esperar a que llegue el mensaje. Solo debe devolver una identidad de operación y registrar qué evento se produjo. La bandeja puede seguir siendo útil, pero ya no es la única fuente de verdad.
En un proyecto reciente, esta separación me resultó más clara junto con una guía para separar emails de signup por escenario. Cada escenario tiene su propio destino, y el evento lleva el contexto suficiente para no confundir registros.
Un contrato de eventos pequeño
No hace falta empezar con una plataforma de mensajería grande. Un contrato mínimo puede tener estos campos:
{
"event_id": "evt_7f21",
"event_type": "verification_email_requested",
"account_id": "acc_204",
"inbox_id": "test-inbox-17",
"created_at": "2026-11-07T14:20:00Z",
"status": "queued"
}
La combinación de event_id e inbox_id es importante. El primero permite deduplicar. El segundo mantiene aislado el correo de un test respecto a los demás. Si usamos un generador de correo temporal para crear el destino, la aplicación debe guardar solo la referencia necesaria para ese caso, no copiar mensajes enteros a cada log.
Los estados pueden ser pocos: queued, sent, delivered y failed. No intentaria representar todas las situaciones del proveedor. El objetivo es que una prueba pueda saber si está esperando un evento que todavía no existe o una entrega que ya falló.
Un endpoint de webhook en FastAPI
El endpoint que recibe una confirmación del proveedor debe ser idempotente. Si llega dos veces el mismo evento, la segunda petición no debe crear una entrega nueva ni cambiar un estado final a otro más confuso.
from datetime import datetime
from typing import Literal
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class EmailEvent(BaseModel):
event_id: str
inbox_id: str
status: Literal["sent", "delivered", "failed"]
occurred_at: datetime
@app.post("/webhooks/email", status_code=204)
async def receive_email_event(event: EmailEvent) -> None:
current = await load_event(event.event_id)
if current is None:
raise HTTPException(status_code=404, detail="event not found")
if current.status in {"delivered", "failed"}:
return
await save_event_status(
event_id=event.event_id,
inbox_id=event.inbox_id,
status=event.status,
occurred_at=event.occurred_at,
)
En código real, load_event y save_event_status estarían detrás de una transacción o una operación condicional. La idea importante es que el evento tenga una clave estable y que la transición sea explícita. Si el proveedor reintenta el webhook, nuestra API responde igual sin duplicar trabajo.
Cómo probar sin carreras
Para un test de signup, prefiero este orden:
- Crear un
inbox_idúnico para el caso. - Crear la cuenta con ese identificador en el contexto de prueba.
- Esperar el evento
verification_email_requested, no tres segundos arbitrarios. - Consultar solo mensajes posteriores a
created_at. - Confirmar que el mensaje pertenece al
event_idy alinbox_id. - Consumir el enlace y guardar la evidencia mínima.
Si el sistema todavía no tiene eventos consultables, se puede envolver el polling en un helper que tenga un límite, una hora de inicio y una razón visible para cada reintento. Eso es mucho mejor que un while True con una pausa fija. Cuando el test falla, el log debe decir si no hubo evento, si hubo un evento sin mensaje o si el mensaje llegó pero no coincidía.
También conviene distinguir un timeout de infraestructura de un rechazo funcional. Un proveedor lento no significa que el token sea invalido. Esta distinción parece pequeña, pero reduce bastante el tiempo de diagnostico.
Checklist para el siguiente flujo
Antes de publicar otro endpoint de verificación, revisaria esto:
- ¿Cada ejecución tiene un
event_idy una bandeja propia? - ¿El endpoint puede recibir el mismo webhook dos veces?
- ¿Los estados terminales se protegen contra cambios accidentales?
- ¿El test espera una condición observable en vez de un tiempo fijo?
- ¿Los logs omiten el contenido sensible del mensaje?
- ¿Se conserva suficiente evidencia para explicar el fallo?
Un compañero puede escribir tempail en una búsqueda o en una nota de QA mientras intenta localizar una herramienta. No pasa nada, pero esa pista no debe convertirse en una regla de negocio ni en una dirección compartida por todos los tests.
La meta no es eliminar por completo el polling. En algunos entornos sigue siendo la integración disponible. La meta es darle un contexto: identidad, límite, evento esperado y motivo de salida. Con ese pequeño contrato, FastAPI queda más fácil de probar, los reintentos dejan mejor evidencia y el equipo puede corregir el punto real del flujo, no solo aumentar el sleep.
Top comments (0)