Cómo probamos MailVault: más de 600 pruebas, correos reales, servidores reales
El correo es una de esas cosas que parecen sencillas por fuera y son cualquier cosa menos eso. Los servidores IMAP se comportan de forma distinta. La codificación MIME tiene casos límite anteriores a la carrera de la mayoría de los desarrolladores. Los tokens de OAuth2 caducan en el peor momento. Los adjuntos pueden estar anidados a tres niveles dentro de mensajes multiparte. Si construyes un cliente de correo, o pruebas de forma obsesiva o publicas fallos.
MailVault tiene ahora mismo más de 600 pruebas automáticas repartidas en cuatro capas: pruebas unitarias en Rust, pruebas unitarias en JavaScript, pruebas de integración contra servidores IMAP reales y pruebas de extremo a extremo que manejan la app de escritorio de verdad. Así encaja todo.
Capa 1: pruebas unitarias en Rust (55 pruebas)
El núcleo de MailVault está escrito en Rust — todas las conexiones IMAP, el envío por SMTP, el análisis MIME y la gestión de tokens de OAuth2 se ejecutan de forma nativa, no a través de un proceso auxiliar de Node.js. La capa de Rust tiene 55 pruebas unitarias que cubren las funciones más difíciles de acertar.
Un buen ejemplo es la compresión de rangos de UID. Cuando MailVault necesita traer las cabeceras de los UID [1, 2, 3, 5, 6, 10], los comprime en la cadena de rangos de IMAP "1:3,5:6,10" para reducir el tamaño del comando. Los tests cubren entradas vacías, UID sueltos, rangos consecutivos, huecos, entradas sin ordenar, duplicados, rangos grandes y la operación inversa (volver a expandir los rangos en UID sueltos). También hay tests de ida y vuelta que comprueban que compress → expand devuelve el conjunto original. Importan porque los comandos UID FETCH por lotes se rompen si los rangos están mal.
El módulo de la API de Microsoft Graph tiene sus propios tests para convertir las respuestas JSON de Graph al formato interno EmailHeader de MailVault, y los tests del módulo principal cubren el análisis de los nombres de archivo de Maildir, la codificación de las marcas y la búsqueda de archivos por UID.
Capa 2: pruebas unitarias en JavaScript (13 suites)
El frontend tiene 13 suites propias de tests unitarios que se ejecutan con Vitest, más tests adicionales a nivel de almacén. Cubren la lógica que está entre el backend en Rust y la interfaz en React:
La interpretación de los correos es la parte con más tests. La suite emailParser ejercita la extracción del corresponsal (convertir "John Doe <[email protected]>" en nombre y dirección), la normalización del asunto (quitando todas las combinaciones de Re:, Fwd:, FW:, Re[N]:), la detección de firmas y la identificación del nivel de cita. El algoritmo de conversaciones — una versión simplificada de la especificación de threading de Jamie Zawinski — tiene tests para las cadenas de In-Reply-To, el recorrido de la cabecera References, la agrupación de reserva por asunto y la detección de ciclos con cabeceras mal formadas.
Otras suites prueban el pipeline en segundo plano (control de concurrencia, reintentos con retroceso exponencial, orden en cascada entre la cuenta activa y las de segundo plano), el manejo de tokens de OAuth2 (refresco proactivo antes de que caduquen, deduplicación de intentos de refresco simultáneos), las optimizaciones de la sincronización delta (el árbol de decisión de cuatro niveles de CONDSTORE), el flujo de carga rápida (cabeceras cacheadas mostradas al instante y sincronización en segundo plano rellenando los cambios), la carga resistente (degradación elegante cuando las conexiones IMAP se caen a mitad de descarga) y las transiciones de estado a lo largo del almacén de Zustand.
Los propios tests del almacén verifican el desalojo de la caché LRU (MailVault guarda en memoria los cuerpos de los correos con un límite configurable), el recorte de campos (quitar rawSource y el contenido de los adjuntos antes de cachear, para ahorrar memoria) y la estimación del tamaño de la caché.
Capa 3: pruebas de integración contra servidores IMAP reales
Las pruebas unitarias con simulaciones solo te dicen hasta cierto punto. Las pruebas de integración de MailVault se conectan a servidores IMAP reales con credenciales reales y envían correos reales. Mantenemos dos cuentas de prueba dedicadas en un dominio privado, y cada ejecución de las pruebas de integración habla con esas cuentas por TLS.
La suite principal, email.test.js, ejecuta un ciclo completo: conectar por IMAP, enviar un correo por SMTP (con Nodemailer), esperar a que llegue, traerlo y analizarlo con mailparser, comprobar las cabeceras y el cuerpo y borrar después el mensaje de prueba en la limpieza. Así se detectan problemas reales que los mocks no ven — rarezas en la negociación TLS, extensiones IMAP propias de un servidor, diferencias de codificación.
La suite de optimizaciones de IMAP prueba las funciones avanzadas del protocolo contra el servidor real: la negociación de COMPRESS=DEFLATE (¿el servidor la admite de verdad? ¿funciona la sesión comprimida?), la sincronización delta con CONDSTORE (¿devuelve CHANGEDSINCE las marcas cambiadas correctas?) y las respuestas compactas de ESEARCH.
La suite de correo entre cuentas prueba escenarios con varias cuentas: enviar de la cuenta A a la cuenta B, comprobar que el mensaje aparece en la bandeja de B, probar la propagación del estado de leído y no leído, y comprobar que el algoritmo de conversaciones enlaza bien la conversación entre las dos cuentas.
La siembra de 500 correos
Para probar de forma realista, sembramos nuestros buzones de prueba con 500 correos: 250 en cada dirección entre las dos cuentas. El script generador crea correos con asuntos realistas ("Informe de ingresos del cuarto trimestre — cifras finales", "Re: fallo en el proceso de pago", "Factura n.º 4821 — vence el 15 de enero") y va rotando cinco tipos MIME: texto plano, HTML, multipart/alternative, HTML con un adjunto y HTML con varios adjuntos (CSV, JSON y PNG).
Alrededor de un tercio de los correos sembrados quedan marcados como no leídos. Los adjuntos incluyen informes en PDF generados, archivos de datos CSV, metadatos JSON e imágenes PNG. Eso nos da un buzón que parece y se comporta como uno real: tipos de contenido mezclados, estados de lectura variados, conversaciones enhebradas y volumen suficiente para que afloren los problemas de rendimiento.
Pruebas de esfuerzo
La suite mailbox-stress.test.js va más allá: inyecta 500 correos en un mismo buzón (recorriendo los cinco tipos MIME) y comprueba que la capa IMAP de MailVault aguanta bien ese volumen. Así se detectan problemas con los comandos UID FETCH por lotes (los partimos en tandas de 200 para no chocar con los límites de longitud de comando de IMAP), con la paginación y con el uso de memoria bajo carga.
Capa 4: pruebas de extremo a extremo en la app real
La última capa maneja la propia app de escritorio de Tauri con WebdriverIO y el WebDriver de Tauri. La app se compila con la característica webdriver activada, se lanza como una ventana de escritorio real y los tests interactúan con ella igual que lo haría una persona: pulsando botones, escribiendo en campos y esperando a que carguen los correos.
Dividimos los tests E2E en dos suites. La suite de interfaz (6 archivos de test) prueba todo lo que no necesita conexión IMAP: los paneles de ajustes, la interacción con la barra lateral, los atajos de teclado, la disposición y el cambio de vista, y las plantillas de redacción. Van rápido porque no tocan la red.
La suite conectada (11 archivos de test) prueba la app completa con cuentas reales — entre ellas una con más de 17.000 correos. No es una prueba sintética: es un buzón real, con años de mensajes acumulados, tipos MIME mezclados, conversaciones anidadas y todos los casos raros que trae una cuenta de correo de larga vida. Ejecutar tests E2E contra un buzón de ese tamaño nos mantiene honestos con la paginación, el uso de memoria y el rendimiento percibido a escala. Estos escenarios incluyen:
Arranque en frío — la app se lanza sin caché, añade una cuenta y mide cuánto tarda en aparecer el correo. Es una medida a la que hacemos seguimiento: los primeros correos visibles partiendo de cero absoluto.
Gestión de cuentas — añadir cuentas, cambiar entre ellas y comprobar que el estado de cada una (último buzón seleccionado, lista de correos en caché) se conserva correctamente.
Lista de correos y visor — recorrer la lista de correos, seleccionarlos, comprobar que los cuerpos cargan bien (desde la caché, desde Maildir o desde IMAP como alternativa) y que los correos contiguos se descargan por adelantado.
Redactar, responder y reenviar — abrir la ventana de redacción, responder a un correo (comprobando que el cuerpo citado y los destinatarios son correctos) y reenviar conservando los adjuntos.
Búsqueda — probar la barra de búsqueda contra el buzón sembrado, comprobando la exactitud de los resultados y el rendimiento.
Operaciones con carpetas — mover correos entre carpetas, archivar y borrar (usando el UID EXPUNGE del RFC 4315 para quitar solo el mensaje indicado).
Bandeja unificada — probar la vista unificada de varias cuentas, que junta los correos de todas ellas en una sola lista cronológica.
Medidas de rendimiento — tiempos de arranque en frío, latencia de carga de correos, rendimiento de las conversaciones. Sacan una tabla resumen con los tiempos p50, p95 y máximo, para poder detectar regresiones.
Cada prueba conectada de extremo a extremo tiene un mecanismo de seguridad: una lista blanca de cuentas de prueba aprobadas. Si una prueba intenta por accidente modificar una cuenta que no está en la lista, falla de inmediato. Así el código de pruebas nunca puede tocar datos reales de nadie durante el desarrollo.
Por qué esto importa
MailVault maneja tu correo. Son datos personales e importantes. Cuando añades una cuenta, cambias de carpeta, archivas un mensaje o buscas en tu bandeja, tiene que funcionar — y no solo en el camino feliz, sino cuando el servidor va lento, cuando se cae la conexión, cuando un correo tiene cabeceras malformadas o cuando un token de OAuth2 caduca a mitad de una descarga.
El enfoque en cuatro capas nos da confianza en todos los niveles: las pruebas de Rust verifican las primitivas de bajo nivel, las unitarias de JavaScript verifican la lógica de la aplicación, las de integración verifican que los servidores reales se comportan como esperamos y las de extremo a extremo verifican que todo el conjunto funciona como app de escritorio. Cuando publicamos una versión, todas ellas han pasado.