Cuando un SaaS empieza a crecer, el email de bienvenida deja de ser un detalle del formulario. Puede activar una cuenta, iniciar una prueba, confirmar un dominio o entregar el acceso a otra persona. Si cada prueba espera que "llegue algo" sin un contrato claro, el equipo termina depurando síntomas: un test intermitente, un usuario duplicado o una bandeja con mensajes que nadie sabe a quién pertenecen.
Una forma sencilla de ordenar este problema es crear una fixture de email: una pequeña pieza de infraestructura que crea una dirección, espera mensajes y devuelve solo la información que la prueba necesita. En este artículo veremos un flujo para un equipo de SaaS pequeño, con palabras simples y decisiones que se pueden aplicar en un backend real.
El problema: el email no es solo un detalle del formulario
Imagina un onboarding con cuatro pasos:
- El usuario escribe su email.
- El backend crea una cuenta pendiente.
- Se envía un enlace de verificación.
- El usuario abre el enlace y pasa a estado activo.
El navegador puede hacer clic correctamente y aun así la prueba fallar. El mensaje puede estar retrasado, puede pertenecer a otro test paralelo o puede contener un enlace viejo. Un sleep(5000) solo oculta la causa y hace que la suite sea mas lenta.
La solución no es pedir que el correo llegue instantaneamente. Es acordar qué significa que el mensaje sea el correcto: destinatario, intención, identificador de ejecución y una ventana de tiempo limitada.
Define el contrato antes de escribir código
Un contrato mínimo para una fixture de correo de usar y tirar puede tener estos campos:
-
address: dirección única para la ejecución. -
runId: identificador corto de la prueba. -
subject: intención esperada, por ejemploConfirma tu cuenta. -
receivedAfter: instante desde el que aceptamos un mensaje. -
link: enlace que la prueba abrirá, sin guardar el cuerpo entero en los logs.
El runId debe viajar en la dirección, en el asunto o en metadatos disponibles para el adaptador. No conviene buscar "el mensaje más reciente" de un buzón compartido, porque dos reintentos pueden intercambiar sus resultados. Ese error parece raro hasta que el CI ejecuta muchos workers a la vez.
También conviene separar dos responsabilidades. La fixture sabe consultar el buzón. El test sabe qué comportamiento del producto debe comprobar. Así, cambiar de proveedor no obliga a reescribir todos los casos de onboarding.
Una fixture pequeña para el backend
Podemos esconder el proveedor detrás de una interfaz pequeña. El ejemplo siguiente no depende de una API concreta; muestra el límite que necesitamos para probar el flujo.
type TestMail = {
id: string;
recipient: string;
subject: string;
verificationUrl: string;
receivedAt: string;
};
type Mailbox = {
createAddress(runId: string): Promise<string>;
listMessages(address: string): Promise<TestMail[]>;
};
async function waitForVerification(
mailbox: Mailbox,
address: string,
runId: string,
timeoutMs = 30_000,
): Promise<TestMail> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const messages = await mailbox.listMessages(address);
const match = messages.find(
(message) =>
message.recipient === address &&
message.subject.includes(runId) &&
message.verificationUrl.startsWith("https://app.example.com/"),
);
if (match) return match;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(`Verification email not received for ${runId}`);
}
En producción, el adaptador debe escapar los datos del proveedor y aplicar límites. No guardes el contenido completo del mensaje en cada log: basta con el ID, el destinatario enmascarado, el asunto y el resultado de la validación. Para entender cómo un feedback estable al enviar emails ayuda a la interfaz, también puedes revisar este patrón de feedback estable al enviar emails.
Cómo usarla en un flujo de onboarding
El caso de prueba queda mas cercano a la intención del usuario:
const runId = `signup-${crypto.randomUUID()}`;
const address = await mailbox.createAddress(runId);
await signupPage.fillEmail(address);
await signupPage.submit();
const mail = await waitForVerification(mailbox, address, runId);
await page.goto(mail.verificationUrl);
await expect(accountPage.status()).toHaveText("Activa");
Para un entorno de desarrollo, una dirección de correo desechable puede servir para separar datos de prueba de una bandeja personal. Aun así, el producto no debería depender de que una persona recuerde borrar mensajes. Define retención corta, no uses datos reales y evita poner tokens en reportes públicos.
La misma idea sirve para operaciones. Si un servicio envía emails después de un despliegue, la prueba debe identificar qué ejecución originó la alerta. Para ampliar el ejemplo hacia operaciones, es útil probar correos de mantenimiento en Kubernetes con la misma disciplina de ownership.
Errores comunes y una lista de comprobación
Estos fallos aparecen mucho cuando se crea la primera versión:
- Compartir una dirección entre workers y confiar en el orden de llegada.
- Usar el asunto como único filtro, sin destinatario ni
runId. - Esperar para siempre cuando el proveedor no responde.
- Hacer que el test conozca cada detalle de la API externa.
- Registrar enlaces completos, aunque contengan un token de acceso.
Una revisión rapida puede preguntar:
- ¿Cada ejecución tiene identidad propia?
- ¿La espera tiene un límite y un intervalo razonable?
- ¿El fallo deja evidencia util, no solo un timeout?
- ¿El adaptador puede reemplazarse sin tocar el onboarding?
- ¿Los mensajes y tokens se eliminan o expiran?
También conviene probar el caso negativo: un mensaje con el destinatario correcto pero con otro runId no debe activar la cuenta. Es una comprobación pequeña y evita errores que son dificiles de ver en una demo.
Preguntas y respuestas rápidas
¿Necesito un proveedor externo?
No siempre. Para una unidad de backend puedes usar un adaptador en memoria. Para una prueba de extremo a extremo necesitas una bandeja aislada o un servicio de pruebas que permita consultar mensajes de forma controlada.
¿Qué hago si el mensaje tarda demasiado?
Guarda un recibo con runId, destinatario enmascarado, intentos y tiempo transcurrido. Eso permite distinguir un bug de la aplicación de una demora del proveedor, en vez de aumentar el timeout a ciegas.
¿Debo aceptar cualquier enlace de verificación?
No. Comprueba el origen, el entorno y la relación con la cuenta creada. Un email recibido no demuestra por sí solo que el enlace sea el esperado.
Recapitulación
Una fixture de email no necesita ser un sistema enorme. Con una identidad por ejecución, una consulta acotada y un contrato claro, el onboarding de un SaaS se vuelve mas fácil de probar. Empieza con un solo flujo, mide qué evidencia necesitas cuando falla y luego comparte el adaptador con el resto del backend. Ese pequeño paso suele ahorrar bastante ruido cuando el producto y el equipo crecen.
Top comments (0)