Un test de registro que pasa una vez y falla en la siguiente suele esconder el mismo problema: el buzón de prueba se está reutilizando. El mensaje anterior sigue ahí, el enlace ya caducó o dos ejecuciones están esperando el mismo correo. El resultado es una prueba frágil y dificil de diagnosticar.
En proyectos de Python he encontrado más estable tratar cada correo como un recurso temporal, no como una constante global. La idea de use and throw email encaja bien aquí: crear un buzón para una corrida, consumir solo el mensaje esperado y dejarlo fuera del siguiente intento. Es una forma sencilla de que la automatización sea más predecible.
El problema de reutilizar un email de prueba
Imagina un endpoint que registra a un usuario y envía un enlace de confirmación. Un test puede usar qa@example.test en cada ejecución, pero pronto aparecen varios problemas:
- Un mensaje viejo coincide con el asunto esperado.
- Dos jobs de CI leen el mismo correo casi al mismo tiempo.
- Un reintento crea otro usuario, pero conserva la identidad anterior.
- El diagnóstico no distingue un fallo de entrega de un fallo de lectura.
Un buzón temporal también puede tener nombres confusos en la documentación interna, como temp org mail o tempail. No importa el nombre comercial o coloquial: el contrato técnico debe decir quién crea el buzón, cuánto vive y qué mensaje puede consumir.
Un contrato pequeño para cada ejecución
Antes de tocar FastAPI, define tres valores por corrida:
-
run_id: identifica el job de CI o la ejecución local. -
mailbox_id: identifica el buzón creado para esa corrida. -
expected_token: relaciona el enlace recibido con el usuario que se está probando.
El run_id evita que dos procesos compartan estado por accidente. El mailbox_id permite limpiar el recurso al terminar. El token, por su parte, evita aceptar cualquier mensaje que tenga el mismo asunto. Puede parecer un poco de trabajo extra, pero ahorra horas cuando un test intermitente aparece solo en el pipeline.
Si todavía no tienes una interfaz para aislar tus pruebas, este enfoque combina bien con aislar las pruebas de email con un contrato. La separación importante es entre el contrato del servicio y el proveedor concreto del buzón.
Implementación en FastAPI
Una interfaz pequeña hace que el endpoint sea fácil de probar. No necesitas que FastAPI conozca todos los detalles del proveedor de correo:
from dataclasses import dataclass
from uuid import uuid4
from fastapi import FastAPI
app = FastAPI()
@dataclass(frozen=True)
class Mailbox:
address: str
mailbox_id: str
def create_test_mailbox(run_id: str) -> Mailbox:
mailbox_id = f"{run_id}-{uuid4().hex[:8]}"
return Mailbox(
address=f"{mailbox_id}@test.invalid",
mailbox_id=mailbox_id,
)
@app.post("/test-users")
def create_test_user(run_id: str):
mailbox = create_test_mailbox(run_id)
token = uuid4().hex
# Guarda mailbox.mailbox_id y token junto al usuario creado.
return {
"email": mailbox.address,
"mailbox_id": mailbox.mailbox_id,
"verification_token": token,
}
En una aplicación real, el endpoint no debería devolver el token. El ejemplo solo muestra la frontera entre la creación del usuario y el recurso de email. El adaptador de pruebas puede guardar el token esperado en el contexto del test y buscar un mensaje cuyo enlace lo contenga.
También conviene no generar el mailbox_id únicamente desde el email. Algunos proveedores normalizan direcciones y eso puede juntar dos buzones que parecian distintos. Guarda el identificador del proveedor como dato de primera clase.
Cómo hacerlo seguro para reintentos
La idempotencia empieza con una clave estable por intención. Por ejemplo, signup:{run_id} puede ser la clave de la operación. Si el test reintenta después de un timeout, el servicio puede devolver el usuario ya creado en vez de enviar tres mensajes diferentes.
Para leer el correo, usa una espera acotada y una condición precisa:
async def wait_for_verification(inbox, token, timeout=30):
deadline = monotonic() + timeout
while monotonic() < deadline:
for message in await inbox.messages():
if token in message.text:
return message
await asyncio.sleep(1)
raise TimeoutError("No llegó el email de verificación")
El código debe distinguir entre “no llegó” y “llegó un mensaje inválido”. El primero puede justificar un reintento de entrega; el segundo señala un problema de plantilla, token o aislamiento. Mezclar ambos estados hace que CI reintente sin necesidad y alarga el diagnóstico.
Al terminar, elimina el buzón o márcalo como cerrado. Si el proveedor no permite borrado inmediato, aplica una política de expiración y no vuelvas a asignar ese identificador. La limpieza que se hace tarde es mejor que ninguna, pero una rutina finally es mucho más confiable.
Checklist para CI
- Crea un buzón nuevo por job o por caso de prueba.
- Incluye
run_idy un token único en los datos de prueba. - Busca el token esperado, no solo el asunto del mensaje.
- Define un timeout y registra la última fase conocida.
- Separa timeout de entrega y contenido inválido.
- Limpia el buzón en
finally, incluso cuando falla la aserción. - Evita escribir direcciones completas y contenido del correo en logs públicos.
Hay un detalle que suele olvidarse: el cleanup también debe ser tolerante a reintentos. Si el buzón ya expiró, la limpieza debe considerarse correcta. De lo contrario, el test verde termina marcado como fallido por una operación secundaria.
Conclusión
Un email de prueba aislado no arregla por sí solo un flujo de autenticación, pero establece una frontera clara. FastAPI puede encargarse del endpoint, mientras un adaptador de testing administra el buzón y la lectura del mensaje. Con run_id, tokens, timeouts y cleanup, cada ejecución sabe qué creó y qué debe consumir.
El resultado es menos ruido en CI y una señal más útil cuando algo rompe. No hace falta una arquitectura enorme: basta con dejar de tratar el buzón de prueba como una variable fija y convertirlo en un recurso con dueño, ciclo de vida y contrato.
Top comments (0)