Aprende a crear fixtures de validación de correo que cubran typos, dominios muertos, catch-all y correos desechables, y a automatizarlos en CI.

Empieza por normalizar la entrada y probar con los casos reales que la gente envía: espacios al inicio/final, dominios en mayúsculas, puntos finales y etiquetas con +. Luego añade fixtures que cubran existencia de dominio, presencia de MX, comportamiento catch-all y detección de desechables para que tus pruebas reflejen los riesgos reales del registro.
Un regex solo te dice que la cadena parece un correo. No puede confirmar que el dominio exista, que pueda recibir correo (registros MX) ni si pertenece a un proveedor desechable, por lo que acabarás aprobando direcciones que rebotarán o atraerán registros de spam.
Divide la validación en capas que puedas probar por separado: sintaxis y normalización (offline), señales de dominio y MX (límite de red), tipo de proveedor (desechable vs normal) y tus reglas de riesgo de negocio. Así las fallas son más fáciles de diagnosticar porque cada fixture está pensado para ser detectado por una capa específica.
Usa resultados gradados como valid, risky, invalid y unknown en lugar de un simple aprobado/reprobado. Los dominios catch-all y los timeouts son la gran razón: a menudo no se puede probar entregabilidad, así que un resultado claro de “riesgoso o desconocido” permite al producto decidir si permitir, advertir o requerir verificación.
Incluye errores tipográficos y problemas de formato, dominios muertos (NXDOMAIN o sin MX), casos catch-all, proveedores desechables y parecidos, y patrones basados en roles o sospechosos como admin@ o cadenas numéricas largas. Mantén cada fixture ligado a una decisión esperada para que pruebes comportamiento y no solo parsing de cadenas.
Trátalos como registros estructurados: correo de entrada, resultado esperado, código de razón y notas. Añade etiquetas como typo, dead_domain, disposable o catch_all para poder ejecutar solo el subconjunto relevante cuando cambies una regla.
Haz que las pruebas unitarias sean totalmente offline simulando resultados de DNS/MX/API, y graba o reinterpreta respuestas para las pruebas de contrato. Las dependencias del mundo real cambian con el tiempo, así que si las usas en CI tendrás fallos aleatorios no relacionados con tu código.
Simula el límite del validador para que el flujo de registro se pruebe de extremo a extremo con respuestas fijas, incluyendo timeouts y errores. Luego mantén un pequeño trabajo de comprobación en vivo (no en cada PR) que haga llamadas reales para detectar desviaciones sin romper todas las solicitudes.
Trátalo como una señal, no como una prueba definitiva, y afirma un resultado ambiguo como “dominio OK, buzón desconocido” en vez de aprobar automáticamente. En lógica de producto, un buen comportamiento por defecto es permitir el registro pero requerir confirmación o añadir límites, y tus pruebas deben fijar esa política.
Fija resultados estructurados esperados por fixture y actualízalos solo cuando cambies la política intencionalmente. Si usas Verimail, puedes mapear fixtures a sus señales de pipeline (syntax, domain, MX, disposable/blocklist) y mantener CI determinista simulando respuestas, mientras ejecutas periódicamente una pequeña suite en vivo para detectar cambios en la clasificación.
La mayoría de los suites de validación de correo parecen sólidas y aun así fallan en producción porque las direcciones reales son desordenadas. La gente pega espacios en blanco, usa etiquetas con plus, mezcla mayúsculas, añade puntos finales, o escribe rápido en móvil y se salta un carácter. Si tus fixtures solo cubren correos limpios y de libro, terminas probando un mundo en el que tus usuarios no viven.
Otro problema común es probar solo la capa más fácil: la sintaxis. Las comprobaciones de sintaxis detectan errores evidentes, pero muchos registros malos usan direcciones que parecen válidas en dominios que no existen, dominios sin configuración de correo, o proveedores desechables que cambian constantemente. Un suite que se queda en un regex crea una falsa sensación de seguridad.
El objetivo no es bloquear todo lo sospechoso. El objetivo es detener registros malos sin bloquear a gente real. Eso significa probar ambos lados: debes rechazar lo que realmente debe rechazarse, y aceptar patrones legítimos y comunes (como plus addressing y subdominios).
Estos patrones de fallo aparecen una y otra vez:
Las buenas pruebas cubren capas que coinciden con cómo funciona la validación en registros reales: sintaxis (reglas estilo RFC), señales de dominio (el dominio existe, registros MX), señales de buzón (comportamiento catch-all o patrones de aceptación), y señales de riesgo (listas negras y detección de desechables).
Fija expectativas desde el principio: algunos resultados son probabilísticos. Los dominios catch-all pueden aceptar cualquier dirección sin probar que exista un buzón. Las listas de desechables evolucionan a diario. Incluso herramientas de grado empresarial como Verimail pueden devolver señales riesgosas que es mejor manejar con reglas de producto (permitir, bloquear o requerir verificación), en vez de fingir que cada caso tiene una respuesta perfecta.
La validación de correo se siente como una sola comprobación, pero en realidad es una cadena de comprobaciones más pequeñas. Divide esa cadena en capas y tus pruebas serán más claras y fáciles de mantener. También te ayuda a construir fixtures que coincidan con registros reales en vez de cadenas aleatorias.
Un modelo de capas práctico se ve así:
No todas las capas deben tratarse igual en las pruebas. Algunas capas son pura lógica y pueden ejecutarse rápido y offline (sintaxis, la mayoría de reglas de riesgo). Otras dependen de la red (DNS, lookup MX, listas negras en tiempo real). Trátalas diferente para que tu suite de pruebas siga estable.
A continuación, decide qué hace tu app con cada resultado de capa. Muchos equipos solo usan pasar o fallar y acaban con casos límite confusos. Un resultado graduado suele ser más fácil de manejar:
Un ejemplo práctico: un usuario introduce [email protected]. La sintaxis pasa, pero tus reglas de sugerencia por typo lo marcan como riesgoso. Podrías permitir el registro pero pedirles que confirmen la dirección. Si introducen [email protected], la sintaxis pasa pero dominio o MX fallan, así que bloqueas.
Cuando escribas pruebas, mapea cada fixture a la capa que debería capturarlo y al resultado esperado. Para capas de red, prefiere pruebas tipo frontera (por ejemplo, cómo manejas una falla de DNS vs un resultado real de sin-MX) y mantiene el resto como pruebas unitarias rápidas. Si usas una API como Verimail, puedes reflejar sus resultados multinivel (sintaxis, dominio, MX, comprobación de desechables) en tus propias expectativas por capa.
Los buenos fixtures de validación de correo parecen lo que la gente realmente escribe, no cadenas aleatorias. Si construyes fixtures alrededor de unas pocas categorías claras de escenarios, las pruebas se mantienen legibles y las lagunas son obvias.
Mantén estas categorías centrales en cada suite:
@, @@ dobles, espacios al inicio/final, puntos dobles, un punto justo antes de @, y caracteres intercambiados en dominios comunes. Estos deben fallar rápido en la capa de sintaxis antes de cualquier comprobación de red.admin@, support@, info@ y patrones sospechosos como cadenas numéricas largas. Prueba tu decisión de política (permitir, advertir o bloquear), en lugar de asumir que siempre son malos.Para que las categorías sean útiles, ata cada fixture a una decisión esperada, no solo válido/inválido. Etiquetas como syntax_invalid, domain_invalid, disposable_block, risky_allow_with_warning, unknown_catch_all hacen que las fallas sean más fáciles de interpretar.
Si usas un validador API como Verimail, mapea estas categorías a las etapas del pipeline que esperas que se activen (sintaxis, comprobaciones de dominio, lookup MX, coincidencia en lista de desechables). Así una prueba fallida te dice qué tipo de fallo fue, no solo que algo rompió.
Los buenos fixtures se sienten como registros reales, no como una pulsación de teclado. Cuando una prueba falla, deberías entender inmediatamente qué pasó y por qué importa.
Trata los fixtures como registros pequeños con una forma consistente: el email de entrada, el resultado esperado (aceptar, rechazar o requerir comprobaciones extra) y una razón corta. Añade un campo de notas para cualquier cosa que tu yo del futuro olvidará (por qué se usa el dominio o qué comportamiento quieres si el proveedor cambia).
Aquí tienes un ejemplo compacto que puedes copiar en tu repo:
[
{
"name": "typo_gmail_missing_dot",
"input": "alex@gmailcom",
"expected": "reject",
"reason": "typo",
"tags": ["typo"],
"notes": "Missing dot in domain; should fail domain validation."
},
{
"name": "disposable_known_provider",
"input": "[email protected]",
"expected": "reject",
"reason": "disposable",
"tags": ["disposable"],
"notes": "Should be blocked by disposable provider list."
}
]
Las etiquetas hacen que la suite sea más fácil de ampliar. En lugar de un archivo gigante, mantiene pequeños conjuntos nombrados por categoría (typos, dominios muertos, catch-all, desechables, casos límite). Así puedes ejecutar solo lo que necesitas, por ejemplo solo casos desechables cuando actualices reglas de clasificación.
Las reglas de normalización también pertenecen a los fixtures, porque la gente pega datos desordenados. Incluye casos que prueben que manejas:
Versiona los fixtures como código. Cada nuevo caso debe responder una pregunta: ¿qué bug evitó esto? Añade una nota corta, exige revisión en cambios y elimina duplicados. Con el tiempo, tus fixtures se convierten en un mapa vivo de los errores y ataques que realmente ve tu registro.
Si dependes de una respuesta de API (por ejemplo, un servicio como Verimail), guarda un resultado esperado fijado por fixture y actualízalo solo cuando cambies la política intencionalmente.
Las comprobaciones de correo tocan cosas que cambian sin aviso: registros DNS, configuraciones de servidor de correo y el ecosistema de proveedores desechables. Si tus fixtures dependen de internet en vivo, las pruebas fallarán un martes cualquiera por razones que no tienen nada que ver con tu código.
Empieza por separar lo que puedes hacer determinista de lo que no. Los casos de sintaxis son fáciles: nunca deberían requerir una llamada de red. La parte complicada es todo lo que involucra alcance de dominio o buzón.
Un conjunto de fixtures estable suele venir de unas pocas fuentes:
Evita usar direcciones de personas reales en fixtures, incluso si son públicas. Usa partes locales sintéticas (como "user" o "test") y nombres claramente falsos. Esto reduce el riesgo de privacidad y evita envíos accidentales si los datos de prueba se filtran en logs o sistemas posteriores.
Para escenarios de dominio muerto, el enfoque más estable es tratar la falla DNS como datos de prueba, no como realidad en vivo. Graba o moquea el resultado del resolvedor (por ejemplo, NXDOMAIN o sin MX) y afirma que tu lógica lo maneja correctamente. Los dominios muertos en vivo son inestables porque pueden ser comprados y configurados más tarde.
La detección de desechables necesita cuidado especial. Los proveedores aparecen, desaparecen y cambian dominios con frecuencia. Mantén una instantánea versionada de tu lista de desechables (o de las respuestas de clasificación de tu proveedor) y decide cómo manejar la deriva. Por ejemplo: las pruebas unitarias corren contra la instantánea, mientras que un trabajo programado verifica cambios y abre tareas de revisión.
Anota cuáles fixtures son sensibles al tiempo y qué harás cuando cambien: actualizar la instantánea, aflojar la aserción o mover el caso a una prueba de contrato. Si usas Verimail en producción, trata su clasificación en tiempo real como algo que pruebas con grabaciones fijadas y luego verifica periódicamente con una pequeña suite en vivo y controlada.
Las pruebas rápidas y fiables empiezan por las partes de la validación que no tocan la red. Trata tu validador como un conjunto de funciones puras (entrada entra, salida sale) y luego blinda el contrato de lo que el resto de tu app puede esperar.
Las pruebas unitarias deben cubrir reglas de sintaxis y normalización, porque son fáciles de romper con pequeños refactors. Ejemplos incluyen recortar espacios, pasar a minúsculas el dominio, rechazar @@ dobles y manejar typos evidentes como falta de puntos en el dominio.
Cuando construyas fixtures, haz que cada fila se explique a sí misma: entrada, valor normalizado esperado (o vacío) y un código de razón corto. Los códigos de razón son más fáciles de afirmar que oraciones completas.
Un patrón simple son pruebas dirigidas por tablas: iterar sobre una tabla de fixtures y afirmar tanto estado como razón para cada caso. Esto mantiene el archivo de pruebas corto y cubre muchos escenarios.
Las pruebas de contrato responden otra pregunta: ¿tu módulo de validación siempre devuelve la misma estructura? Si un equipo espera { status, reason, normalized } y otro release lo cambia en silencio, obtienes bugs que parecen aleatorios.
Las pruebas de contrato deberían verificar cosas como:
status, reason, normalized_email)valid, invalid, risky)Las pruebas basadas en propiedades también ayudan aquí. En vez de escribir a mano cada typo, genera casi-errores: espacios extra, caracteres intercambiados alrededor de la @, puntos repetidos o dominios en mayúsculas mezcladas. El objetivo es atrapar bugs del parser y casos límite que no pensaste incluir.
Las pruebas de snapshot pueden ayudar para mensajes visibles en UI, pero úsalas con cuidado. Prefiere capturar códigos de error estables, no texto completo. Si debes fijar mensajes, mantenlos cortos y consistentes para que pequeños cambios de copy no rompan la mitad de tu suite.
Las pruebas de integración son donde la lógica de validación de correo suele volverse poco fiable. En el momento en que una prueba depende de DNS real, lookups MX o un servicio de terceros accesible, puedes tener fallos aleatorios que no tienen nada que ver con tu código.
Para las ejecuciones diarias en CI, apunta a pruebas rápidas y repetibles. Trata la red como una entrada que controlas.
Un enfoque práctico es moquear el límite de la red y centrarte en lo que hace tu app con cada resultado. Si tu servicio de registro llama a una API de validación, reemplaza esa llamada con un stub que devuelva respuestas conocidas para tus fixtures. Sigues probando el flujo completo (controlador, servicio, decisión de política, mensaje de error), pero no pruebas internet.
Patrones que suelen funcionar bien:
Los timeouts merecen atención especial. Decide tu política y pruébala explícitamente: cuando la validación agota el tiempo, ¿tratas el correo como inválido o como desconocido y permites avanzar con verificación extra? Ambas pueden ser correctas, pero solo si son consistentes.
Las pruebas mockeadas pueden desviarse de la realidad. Un dominio que tenía registros MX puede quedar muerto. Una clasificación de desechable puede cambiar. Para detectar la deriva, mantiene un trabajo separado que haga llamadas reales, pero no lo ejecute en cada pull request.
Una configuración típica es una ejecución nocturna o un workflow de comprobación manual. Mantén esta pista pequeña: un puñado de correos representativos por categoría (typos, dominios muertos, catch-all, desechables) es suficiente para detectar deriva sin generar ruido.
Si usas Verimail en producción, tus pruebas en CI pueden simular su respuesta disposable: true para un fixture como [email protected], y afirmas que la UI bloquea el registro y muestra el mensaje correcto. Luego tu job nocturno puede llamar a la API real con un conjunto controlado de direcciones y alertarte si los resultados cambian, para que actualices fixtures o ajustes la política antes de que los usuarios lo noten.
Un suite de pruebas puede verse ocupado y aun así perder las fallas que te perjudican en producción. El mayor riesgo es la falsa confianza: las pruebas pasan, pero usuarios reales quedan bloqueados, o registros falsos se cuelan.
Una trampa común es tratar a los dominios catch-all como prueba de que el buzón existe. Catch-all solo significa que el dominio acepta correo para cualquier dirección. Si tu lógica aprueba todo automáticamente en un catch-all, tus fixtures te entrenan en aceptar direcciones basura.
Otra trampa es usar solo un regex y llamarlo validación. El regex puede atrapar problemas de formato, pero no te dice si el dominio existe, si tiene registros MX o si la dirección pertenece a un proveedor desechable. Si tus pruebas solo cubren patrones de cadena, estás probando tu regex, no el comportamiento del correo.
Fallarlo duro en registros por problemas DNS temporales también es un error frecuente. Las redes reales tienen timeouts y fallas intermitentes. Si tus pruebas solo cubren DNS funciona y DNS falla, puedes acabar rechazando buenos usuarios durante una breve caída. Un mejor enfoque es tratar algunos errores como desconocidos y reintentar, o permitir el registro pero marcar la dirección para verificación posterior.
Los dominios internacionalizados y las reglas modernas de buzón son fáciles de olvidar. El plus-addressing (como [email protected]) es válido y se usa mucho. Algunos usuarios también tienen dominios no ASCII. Si tus fixtures nunca incluyen estos casos, lanzarás un validador que rompe en entradas normales.
Algunas maneras en que los equipos se engañan a sí mismos:
+ es inválido o eliminarlo incorrectamente.Una salvaguarda práctica es separar resultados en cubetas claras (invalid, risky, unknown, valid) y probar las transiciones entre ellas. Herramientas como Verimail ayudan devolviendo señales estructuradas (syntax, domain, MX, disposable), lo que facilita afirmar el comportamiento sin adivinar a partir de un único aprobado/fallado.
Antes de hacer merge, haz una pasada rápida por cobertura y confianza. El objetivo no es probar cada correo del planeta, sino asegurarte de que los fixtures se comportan como registros reales y fallan de formas previsibles.
Revisa tu conjunto de fixtures por categoría. Si una categoría solo tiene uno o dos ejemplos, un pequeño cambio de código puede romper a usuarios reales y tus pruebas seguirán verdes. Apunta a un pequeño conjunto de casos por categoría para atrapar casos límite (por ejemplo un typo que aún parece válido).
Usa esta lista corta:
No ignores la estabilidad. Las pruebas que dependen de dominios reales pueden pudrirse con el tiempo, y el comportamiento catch-all puede cambiar sin aviso. Para pruebas unitarias, prefiere fixtures que no requieran la red y mantiene el comportamiento de dominio como respuestas moqueadas. Para las pruebas de integración, limita el alcance y hazlas intencionales. Un patrón simple es un trabajo nocturno pequeño que ejercita unos pocos casos conocidos contra tu servicio de validación (o proveedor), mientras CI sigue centrado en pruebas unitarias deterministas.
Un problema común en SaaS: tu formulario de registro parece bien en pruebas manuales, pero al lanzarlo empiezas a ver oleadas de registros spam. Muchos usan proveedores desechables, lo que significa malos leads, más rebotes y una base de usuarios desordenada.
Un enfoque práctico es definir comportamiento claro y fijarlo con fixtures. Por ejemplo: bloquear direcciones desechables, rechazar dominios muertos y permitir (pero advertir) dominios catch-all.
Aquí tienes un pequeño conjunto de fixtures que cubre entradas realistas sin depender de cadenas aleatorias:
[email protected] (parece real, pero el dominio está mal escrito)[email protected] (usa .invalid para representar un dominio que nunca debería resolver)[email protected] (trata esto como catch-all en mocks, no como un hecho DNS real)[email protected] (trata esto como desechable en mocks, no como un proveedor real)[email protected] (una dirección con apariencia normal para la ruta feliz)La clave es que tu app no debería intentar probar catch-all o estado desechable usando internet público durante pruebas unitarias. En su lugar, tu capa de validación debe devolver un resultado normalizado sobre el que la lógica de registro pueda actuar.
Una regla simple para el endpoint de registro podría ser:
is_disposable = true: bloquear el registro con un error clarodomain_status = dead: rechazar y pedir otro correois_catch_all = true: permitir el registro, pero mostrar una advertencia (y considerar verificación extra)Para mantener CI rápido y predecible, divide las pruebas en dos velocidades: pruebas unitarias rápidas para tu lógica de decisión, y pruebas de integración simuladas para el límite del validador.
// Example: decision logic unit tests (no network)
const cases = [
{ email: "[email protected]", result: { is_disposable: true }, expect: "BLOCK" },
{ email: "[email protected]", result: { domain_status: "dead" }, expect: "REJECT" },
{ email: "[email protected]", result: { is_catch_all: true }, expect: "WARN" },
{ email: "[email protected]", result: { suggestion: "gmail.com" }, expect: "REJECT" },
{ email: "[email protected]", result: { ok: true }, expect: "ALLOW" },
];
for (const c of cases) {
const decision = decideSignup(c.email, c.result);
expect(decision).toBe(c.expect);
}
En CI, la prueba de integración simulada puede verificar que tu código de registro llama al validador una vez y maneja timeouts y respuestas de error, sin hacer realmente lookups DNS o MX.
Si quieres comprobaciones reales (sintaxis RFC, verificación de dominio, lookup MX y coincidencia en listas de desechables/blacklists) sin construir y mantener toda esa lógica, una API de validación de correo puede encajar bien. Para equipos que ya usan Verimail, un patrón simple es mantener la mayoría de pruebas simuladas y deterministas, luego ejecutar un pequeño conjunto de comprobaciones de contrato para asegurarte de que tu integración con verimail.co sigue coincidiendo con la estructura de respuestas que tu app espera.