Un email de verificación puede romper un test aunque el endpoint funcione bien. El mensaje llega tarde, el polling encuentra una copia vieja o dos ejecuciones leen la misma bandeja. Luego el equipo repite el pipeline y la causa queda medio escondida.
Una forma práctica de ordenar este problema es tratar la bandeja de prueba como un recurso alquilado: cada ejecución obtiene un lease, una identidad, una hora de comienzo y una fecha de expiración. El test solo puede leer mensajes que pertenecen a ese periodo. No es una solución magica, pero convierte un fallo confuso en un contrato que se puede inspeccionar.
El problema: una bandeja que nadie posee
En un flujo de registro, la aplicación suele enviar un enlace a un correo temporal para pruebas. Si todos los tests usan la misma dirección, aparecen tres riesgos:
- una ejecución consume el mensaje de otra;
- el filtro por asunto elige un email antiguo;
- un reintento parece pasar, pero nadie sabe que uso datos de la primera ejecución.
He visto este patrón en automatizaciones pequeñas: primero se añade un sleep(5), despues otro reintento y finalmente un timeout más largo. El pipeline tarda más, pero no es mas confiable. Incluso búsquedas como temp gamil com o fake e mail com en notas de QA suelen mostrar que la convención de datos no está clara.
La idea del lease es sencilla: el sistema que prepara el test crea una reserva con un identificador único. Ese identificador acompaña la petición de verificación y también queda guardado junto al mensaje esperado.
Qué debe representar un lease
Un lease útil necesita pocos campos, pero tienen que ser explícitos:
-
lease_id: UUID de la ejecución o del caso de prueba. -
mailbox: dirección aislada para recibir el mensaje. -
created_at: límite inferior para ignorar mensajes viejos. -
expires_at: límite superior para no dejar polling infinito. -
status: activo, consumido o expirado.
Con esto, el test deja de preguntar “¿cuál es el último email?” y pregunta “¿cuál es el mensaje de mi lease, creado después de mi acción?”. Esa diferencia parece pequeña, pero hace el diagnóstico bastante mas facil.
También conviene que el lease sea idempotente. Si el runner repite la llamada con el mismo lease_id, la API debe devolver la reserva existente, no crear otra bandeja que después nadie limpia.
Un contrato pequeño en FastAPI
Este ejemplo solo modela la parte de control. El proveedor real de correo se puede conectar detrás de MailboxService sin mezclarlo con las rutas HTTP.
from datetime import datetime, timedelta, timezone from uuid import UUID, uuid4 from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field app = FastAPI() leases: dict[UUID, dict] = {} class LeaseRequest(BaseModel): mailbox: str = Field(min_length=3) ttl_seconds: int = Field(default=180, ge=30, le=900) @app.post("/test-email/leases", status_code=201) def create_lease(request: LeaseRequest): now = datetime.now(timezone.utc) lease_id = uuid4() lease = { "lease_id": lease_id, "mailbox": request.mailbox, "created_at": now, "expires_at": now + timedelta(seconds=request.ttl_seconds), "status": "active", } leases[lease_id] = lease return lease @app.get("/test-email/leases/{lease_id}") def get_lease(lease_id: UUID): lease = leases.get(lease_id) if lease is None: raise HTTPException(status_code=404, detail="Lease no encontrado") return lease En producción, el diccionario debe ser una tabla o un almacén con expiración. El ejemplo sirve para mostrar el límite: la API es dueña del estado del lease y el lector de emails no necesita adivinarlo.
Polling con límites y liberación
El consumidor puede enviar lease_id al servicio que consulta la bandeja. El servicio aplica estos filtros en este orden:
- comprobar que el lease existe y sigue activo;
- descartar mensajes con fecha anterior a
created_at; - comprobar que el destinatario y el asunto corresponden al caso;
- marcar el mensaje como consumido después de validarlo;
- liberar la bandeja al terminar, también si el test falla.
La última parte se olvida bastante. En Python, una función de cleanup llamada desde finally suele ser suficiente para el runner. Si el proveedor permite una clave de correlación, úsala junto al lease_id; el asunto por sí solo es demasiado debil.
El timeout debe pertenecer al contrato, no estar repartido entre tres helpers. Por ejemplo, un lease de 180 segundos puede tener polling cada dos segundos, pero debe devolver una razón clara al expirar: lease_expired, message_not_found o message_rejected. Un error así ayuda mucho mas que “Timeout waiting for email”.
Para un flujo completo, también vale la pena probar upgrades por email sin ruido y revisar cómo mantener pruebas de email sin deriva en CI. Son piezas distintas, pero comparten la misma regla: cada ejecución debe dejar evidencia propia.
Cómo llevarlo a CI
Antes de lanzar el navegador o la llamada API, el job debe:
- crear el lease y guardar su JSON como artefacto;
- pasar
lease_idymailboxcomo variables del test; - registrar el momento exacto en que se dispara el email;
- adjuntar el mensaje seleccionado y la razón de selección;
- ejecutar la liberación en un paso de cleanup.
Cuando hay reintentos, conserva los artefactos por intento. Sobrescribirlos es tentador, pero hace que el segundo intento borre la evidencia del primero. Un nombre como email-lease-{lease_id}.json ya ayuda a mantener el contexto.
Preguntas rápidas
¿Un lease reemplaza una dirección aislada por test?
No. La dirección aislada sigue siendo buena práctica. El lease añade tiempo, propiedad y estado, así que protege cuando la dirección se reutiliza o el proveedor no puede crear una nueva para cada caso.
¿Debo guardar el contenido completo del email?
Solo si es necesario para depurar. En muchos casos basta con guardar asunto, remitente, fecha, identificador y un hash del cuerpo. Así reduces datos sensibles en los artefactos de CI.
¿Qué hago si el proveedor no tiene expiración?
Mantén la expiración en tu propia base de datos y ejecuta una tarea de limpieza. La ausencia de una función remota no debe convertir el polling en un proceso sin fin.
Cierre
Un contrato de lease no hace que el correo sea instantáneo. Hace algo más útil: define qué ejecución tiene derecho a leer qué mensaje y durante cuánto tiempo. Con FastAPI, unas rutas pequeñas, un lease_id y un cleanup fiable bastan para empezar.
La próxima vez que un test de verificación falle, busca primero la propiedad del inbox y la ventana temporal. Si esas dos cosas están documentadas, el equipo puede arreglar el sistema en vez de solo subir el timeout.


