Cuando una prueba de registro falla porque encontró un correo viejo, normalmente no es un problema de FastAPI. Es un problema de aislamiento. En varios equipos he visto que se usa un buzón compartido para todo el pipeline y luego cada ejecución tiene que adivinar cual mensaje le pertenece. Funciona durante un tiempo, hasta que dos jobs corren juntos o llega un reintento tarde.
Mi enfoque para estos flujos es tratar el buzón de prueba como un recurso con contrato: nace con la ejecución, tiene un identificador propio, expira y deja una evidencia pequeña antes de limpiarse. La idea sirve para una prueba de verificación, un flujo de recuperación de contraseña o cualquier API que dependa de un email entrante.
El problema: un buzón compartido contamina las pruebas
Un test típico hace esto:
- Crea un usuario con un correo de prueba.
- Llama al endpoint de signup de FastAPI.
- Espera el mensaje de verificación.
- Extrae el enlace y confirma la cuenta.
Si todas las ejecuciones usan la misma dirección, el paso tres puede leer un mensaje de otra rama o de un intento anterior. Un subject parecido no alcanza: dos tests pueden tener el mismo asunto y destinatario. El fallo aparece de forma intermitente, que es la forma más cara de depurar.
También conviene separar la preparación de datos del consumo del mensaje. Una seed list para un onboarding reproducible ayuda a ordenar usuarios y estados, pero no reemplaza un inbox aislado. La preparación de seed lists para un onboarding reproducible puede vivir en una etapa anterior; el contrato de correo debe seguir siendo específico para cada ejecución.
El contrato mínimo de un buzón por ejecución
Antes de escribir código, defino cinco reglas sencillas:
-
Identidad: la dirección incluye un
run_ido un token aleatorio. - Propiedad: el test conoce qué mensajes puede consumir.
- Caducidad: el recurso no debe quedarse vivo después del job.
- Espera acotada: nunca se espera para siempre.
- Evidencia: asunto, destinatario normalizado y resultado quedan en el log; no el contenido completo si contiene datos sensibles.
Esto es más útil que elegir un proveedor por la etiqueta de “throwaway email”. Incluso cuando se usa un burner email address para pruebas manuales, el pipeline necesita una política de propiedad y limpieza. Las variantes escritas como temp mailid o tempail aparecen a veces en búsquedas y tickets, pero no deben terminar mezcladas con el nombre de una fixture ni con un selector del test.
Una fixture pequeña en Python
La interfaz de la fixture puede ser independiente del proveedor. Así el test solo conoce las operaciones que necesita:
from dataclasses import dataclass
from uuid import uuid4
@dataclass(frozen=True)
class TestInbox:
address: str
run_id: str
def create_inbox(domain: str = "qa.example") -> TestInbox:
run_id = uuid4().hex[:12]
return TestInbox(
address=f"signup-{run_id}@{domain}",
run_id=run_id,
)
En un sistema real, create_inbox también reservaría la dirección en el proveedor elegido. Lo importante es que devuelva el identificador que usaremos al buscar mensajes. No conviene generar una dirección en el test y buscar después por texto parcial: ese atajo hace que el aislamiento dependa de la suerte.
El test puede pasar inbox.address al endpoint y conservar inbox.run_id como correlación. Para consultar el mensaje, filtraría por destinatario exacto, una marca de ejecución en los headers o un token único dentro del enlace. Si el token aparece en la URL, no lo imprimas completo en CI.
Cómo esperar y limpiar sin hacer el test frágil
Leer el inbox una sola vez suele ser demasiado optimista. El correo es asíncrono, así que prefiero polling con pausa corta y un límite total. Cada intento debe tener una condición clara:
import time
def wait_for_message(client, inbox: TestInbox, timeout: float = 20.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
messages = client.list_messages(to=inbox.address)
match = next((m for m in messages if inbox.run_id in m.text), None)
if match:
return match
time.sleep(0.5)
raise TimeoutError(f"No llegó el email para {inbox.run_id}")
El timeout debe representar el contrato del entorno de pruebas, no una espera infinita disfrazada. Si el proveedor ofrece webhook, puede sustituir al polling; la misma correlación sigue siendo necesaria.
Limpio en un bloque finally, incluso cuando falla la aserción. Si la limpieza falla, la registro como una alerta de infraestructura sin ocultar el error principal del test. Y si el sistema bajo prueba necesita reintentos, el test debe verificar que usa el mismo intento lógico, no aceptar cualquier mensaje que aparezca.
Qué guardar como evidencia en CI
Una evidencia útil es pequeña y suficiente:
-
run_idde la ejecución; - dirección de prueba, parcialmente enmascarada;
- timestamp de creación y de recepción;
- identificador del mensaje;
- estado final: recibido, timeout o limpieza fallida.
No guardes el HTML completo por defecto. Puede contener tokens de recuperación o datos personales de una prueba. Un snapshot del estado y el mensaje de error suele bastar para reproducir el problema. Si el equipo necesita ver la UI, aplica el mismo criterio de aislamiento a cada sesión; mostrar errores inline sin mover el formulario también depende de que los datos de prueba no se pisen entre sí.
Preguntas frecuentes
¿Puedo usar una sola dirección para todos los tests?
Solo para una prueba local muy controlada. En CI, una dirección por ejecución reduce colisiones y hace el fallo más explicable.
¿Debo borrar el buzón después de cada test?
Sí, o al menos marcarlo para una limpieza con TTL. El finally es la red de seguridad cuando una aserción se rompe.
¿Qué hago si el email tarda más de lo esperado?
Registra el timeout y los metadatos de entrega. Luego revisa proveedor, cola y endpoint por separado. Aumentar el timeout sin evidencia solo vuelve el pipeline más lento.
El patrón no requiere una arquitectura grande: identidad, correlación, espera limitada y limpieza. Con esas piezas, una prueba de email en FastAPI deja de depender de mensajes casuales en un buzón compartido y la automatización se vuelve bastante más tranquila.
Top comments (0)