Apprenez à créer des jeux de tests de validation d'e-mails couvrant fautes de frappe, domaines morts, catch-all et adresses jetables, et à les automatiser dans le CI.

Commencez par normaliser l'entrée et testez les éléments que les utilisateurs soumettent réellement : espaces en début/fin, domaines en majuscules, points finaux, et plus-tags. Ajoutez ensuite des fixtures qui couvrent l'existence du domaine, la présence de MX, le comportement catch-all et la détection des adresses jetables afin que vos tests reflètent les vrais risques d'inscription.
Un regex indique seulement que la chaîne ressemble à une adresse e-mail. Il ne peut pas confirmer que le domaine existe, qu'il peut recevoir du courrier (enregistrements MX) ou qu'il n'appartient pas à un fournisseur jetable. Vous risquez donc d'approuver des adresses qui rebondiront ou attireront des inscriptions indésirables.
Séparez la validation en couches testables : syntaxe et normalisation (hors réseau), signaux de domaine et MX (limite réseau), type de fournisseur (jetable vs normal) et vos règles métier. Cela facilite le diagnostic, car chaque fixture doit être détectée par une couche précise.
Utilisez des résultats gradués comme valid, risky, invalid et unknown plutôt qu'un simple pass/fail. Les domaines catch-all et les délais d'attente sont les principales raisons : souvent on ne peut pas prouver la délivrabilité, et une sortie « risquée » ou « inconnue » permet au produit de décider d'autoriser, d'avertir ou d'exiger une vérification.
Incluez fautes de frappe et erreurs de formatage, domaines morts (NXDOMAIN ou pas de MX), cas catch-all, fournisseurs jetables et leurs imitations, ainsi que des motifs basés sur des rôles ou suspects comme admin@ ou de longues suites numériques. Attachez à chaque fixture une décision attendue pour tester le comportement, pas seulement le parsing.
Considérez-les comme des enregistrements structurés : e-mail d'entrée, résultat attendu, code de raison et notes. Ajoutez des tags comme typo, dead_domain, disposable ou catch_all pour exécuter seulement les sous-ensembles pertinents lors d'un changement de règle.
Rendez les tests unitaires entièrement hors réseau en simulant les résultats DNS/MX/API, et enregistrez ou rejouez les réponses pour les tests de contrat. Les dépendances internet changent avec le temps : si vous les laissez dans le CI, vous aurez des échecs aléatoires indépendants de votre code.
Simulez la frontière du validateur pour tester le flux d'inscription de bout en bout avec des réponses fixes, y compris les délais et les erreurs. Ensuite, maintenez une petite exécution live (quotidienne ou manuelle) qui interroge réellement le DNS ou une API pour détecter la dérive sans bloquer chaque pull request.
Considérez-le comme un signal, pas comme une preuve. Attendez-vous à un résultat ambigu comme « domaine OK, boîte inconnue » plutôt qu'à une approbation automatique. En pratique, autoriser l'inscription mais exiger une confirmation ou appliquer des limites est un bon comportement, et vos tests doivent verrouiller cette politique.
Fixez un résultat structuré attendu par fixture et ne le mettez à jour que lorsque vous changez volontairement la politique. Si vous utilisez Verimail, mappez les fixtures sur ses signaux en pipeline (syntax, domain, MX, disposable/blocklist), simulez les réponses en CI pour la déterminisme et exécutez périodiquement une petite suite live pour détecter les changements de classification.
La plupart des suites de tests de validation d'e-mails semblent solides et échouent quand même en production parce que les adresses réelles sont désordonnées. Les gens collent des espaces, utilisent des plus-tags, mélangent les majuscules, ajoutent des points finaux, ou tapent vite sur mobile et oublient un caractère. Si vos fixtures ne couvrent que des e-mails propres et formatés, vous testez un monde où vos utilisateurs ne vivent pas.
Un autre problème fréquent est de ne tester que la couche la plus simple : la syntaxe. Les vérifications de syntaxe attrapent des erreurs évidentes, mais beaucoup de mauvaises inscriptions utilisent des adresses qui semblent valides sur des domaines qui n'existent pas, des domaines sans configuration mail ou des fournisseurs jetables qui changent constamment. Une suite qui s'arrête à une regex donne un faux sentiment de sécurité.
L'objectif n'est pas de bloquer tout ce qui semble suspect. L'objectif est d'arrêter les mauvaises inscriptions sans bloquer les vraies personnes. Cela signifie tester les deux côtés : rejeter ce qui doit l'être, et accepter les schémas légitimes courants (comme le plus-addressing et les sous-domaines).
Ces motifs d'échec reviennent souvent :
De bons tests couvrent des couches qui correspondent à la façon dont la validation fonctionne dans de vraies inscriptions : syntaxe (règles de type RFC), signaux de domaine (le domaine existe-t-il, enregistrements MX), signaux de boîte (comportement catch-all ou modèles d'acceptation) et signaux de risque (listes noires et détection de jetables).
Mettez les attentes en place tôt : certains résultats sont probabilistes. Les domaines catch-all peuvent accepter n'importe quelle adresse sans prouver l'existence d'une boîte. Les listes d'adresses jetables évoluent quotidiennement. Même des outils de qualité entreprise comme Verimail peuvent renvoyer des signaux risqués qui sont mieux traités par des règles produit (autoriser, bloquer ou exiger une vérification supplémentaire), plutôt que de prétendre que chaque cas a une réponse parfaite.
La validation d'e-mails ressemble à un seul contrôle, mais c'est en réalité une chaîne de vérifications plus petites. Scindez cette chaîne en couches et vos tests deviennent plus clairs et plus faciles à maintenir. Cela vous aide aussi à construire des fixtures qui correspondent aux inscriptions réelles des utilisateurs au lieu de chaînes aléatoires.
Un modèle de couches pratique ressemble à ceci :
Toutes les couches ne doivent pas être traitées de la même façon dans les tests. Certaines couches sont de la logique pure et peuvent s'exécuter rapidement hors ligne (syntax, la plupart des règles de risque). D'autres dépendent du réseau (résolution DNS, recherche MX, listes noires en temps réel). Traitez-les différemment afin que votre suite reste stable.
Ensuite, décidez ce que votre application fait avec chaque résultat de couche. Beaucoup d'équipes n'utilisent que passer ou échouer, puis se retrouvent avec des cas limites confus. Un résultat gradé est généralement plus simple à gérer :
Un exemple pratique : un utilisateur saisit [email protected]. La syntaxe passe, mais vos règles de détection de fautes marquent cela comme risqué. Vous pouvez autoriser l'inscription mais leur demander de confirmer l'adresse. S'ils saisissent [email protected], la syntaxe passe mais le domaine ou le MX échoue, donc vous bloquez.
Quand vous écrivez des tests, mappez chaque fixture à la couche qui doit la détecter et au résultat attendu. Pour les couches réseau, préférez des tests de style frontière (par exemple, comment gérer une panne DNS vs un vrai résultat no-MX) et gardez le reste comme tests unitaires rapides. Si vous utilisez une API comme Verimail, vous pouvez reproduire ses résultats multi-étapes (syntax, domaine, MX, vérifications jetables) dans vos propres attentes de couche.
De bons jeux de tests de validation d'e-mails ressemblent à ce que les vrais utilisateurs tapent, pas à des chaînes aléatoires. Si vous construisez vos fixtures autour de quelques catégories claires, les tests restent lisibles et les lacunes deviennent évidentes.
Gardez ces catégories de base dans chaque suite :
@ manquant, double @@, espaces en début/fin, doubles points, un point juste avant @, et caractères échangés dans des domaines courants. Ceux-ci doivent échouer rapidement au niveau syntaxe avant toute vérification réseau.admin@, support@, info@ et motifs suspects comme de longues chaînes numériques. Testez votre décision de politique (autoriser, avertir ou bloquer), au lieu de supposer qu'ils sont toujours mauvais.Pour que les catégories restent utiles, reliez chaque fixture à une décision attendue, pas seulement valide/invalide. Des étiquettes comme syntax_invalid, domain_invalid, disposable_block, risky_allow_with_warning, unknown_catch_all rendent les échecs plus faciles à interpréter.
Si vous utilisez un validateur API comme Verimail, mappez ces catégories aux étapes du pipeline que vous vous attendez à déclencher (syntax, vérifications de domaine, recherche MX, correspondance blocklist jetable). De cette façon, un test qui échoue vous dit quel type d'échec c'était, et non pas juste que quelque chose s'est cassé.
De bonnes fixtures ressemblent à de vraies inscriptions, pas à des frappes au hasard. Quand un test échoue, vous devez comprendre immédiatement ce qui s'est passé et pourquoi cela compte.
Traitez les fixtures comme de petits enregistrements avec une forme cohérente : e-mail d'entrée, résultat attendu (accepter, rejeter ou exiger des vérifications supplémentaires) et une brève raison. Ajoutez un champ notes pour tout ce que vous oublierez plus tard (pourquoi le domaine est utilisé, ou quel comportement vous souhaitez si le fournisseur change).
Voici un exemple compact que vous pouvez copier dans votre 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."
}
]
Les tags rendent la suite plus facile à faire évoluer. Au lieu d'un seul gros fichier, conservez de petits ensembles nommés par catégorie (fautes, domaines morts, catch-all, jetables, cas limites). Ainsi vous pouvez exécuter uniquement ce dont vous avez besoin, par exemple seulement les cas jetables quand vous mettez à jour les règles de classification.
Les règles de normalisation doivent aussi faire partie des fixtures, car les utilisateurs collent des données désordonnées. Incluez des cas qui prouvent que vous gérez :
Versionnez les fixtures comme du code. Chaque nouveau cas doit répondre à une question : quel bug cela a-t-il évité ? Ajoutez une note courte, exigez une revue sur les changements et supprimez les doublons. Avec le temps, vos fixtures deviennent une carte vivante des erreurs et attaques réelles que vos inscriptions rencontrent.
Si vous vous basez sur une réponse d'API (par exemple un service comme Verimail), enregistrez un résultat attendu épinglé par fixture et ne le mettez à jour que lorsque vous changez délibérément la politique.
Les vérifications d'e-mails touchent des choses qui changent sans prévenir : enregistrements DNS, configurations de serveurs mail et écosystème des fournisseurs jetables. Si vos fixtures dépendent d'internet en direct, les tests échoueront un mardi au hasard pour des raisons qui n'ont rien à voir avec votre code.
Commencez par séparer ce que vous pouvez rendre déterministe de ce que vous ne pouvez pas. Les cas de syntaxe sont faciles : ils ne devraient jamais nécessiter d'appel réseau. La partie délicate concerne tout ce qui implique l'atteignabilité du domaine ou de la boîte.
Un ensemble de fixtures stable provient généralement de quelques sources :
Évitez d'utiliser des adresses de vraies personnes dans les fixtures, même si elles sont publiques. Utilisez des parties locales synthétiques (comme "user" ou "test") et des noms clairement factices. Cela réduit les risques de confidentialité et empêche les envois accidentels si des données de test fuient dans les logs ou des systèmes en aval.
Pour les scénarios de domaine mort, l'approche la plus stable est de traiter la défaillance DNS comme des données de test, pas comme une réalité vivante. Enregistrez ou simulez le résultat du résolveur (par exemple NXDOMAIN ou pas de MX) et affirmez que votre logique le gère correctement. Les domaines morts en direct sont instables car ils peuvent être achetés et configurés plus tard.
La détection des jetables nécessite un soin particulier. Les fournisseurs apparaissent, disparaissent et changent de domaines souvent. Conservez un instantané versionné de votre liste jetable (ou des réponses de classification de votre fournisseur) et décidez comment gérer la dérive. Par exemple : les tests unitaires s'exécutent contre l'instantané, tandis qu'un job planifié vérifie les changements et ouvre une tâche de revue.
Documentez quelles fixtures sont sensibles au temps et ce que vous ferez quand elles changeront : mettre à jour l'instantané, assouplir l'assertion ou déplacer le cas en test de contrat. Si vous utilisez Verimail en production, traitez sa classification en temps réel comme quelque chose que vous testez avec des enregistrements épinglés, puis vérifiez périodiquement avec une petite suite live contrôlée.
Des tests rapides et fiables commencent par les parties de la validation qui ne touchent pas le réseau. Traitez votre validateur comme un ensemble de fonctions pures (entrée en, sortie out), puis verrouillez le contrat sur ce que le reste de votre app peut attendre.
Les tests unitaires doivent couvrir les règles de syntaxe et la normalisation, car ce sont des choses faciles à casser lors de petits refactorings. Exemples : suppression des espaces, mise en minuscules du domaine, rejet des doubles @ et gestion des fautes évidentes comme l'absence d'un point dans le domaine.
Quand vous construisez des fixtures, faites en sorte que chaque ligne s'explique elle-même : entrée, valeur normalisée attendue (ou vide) et un court code de raison. Les codes de raison sont plus faciles à vérifier que des phrases entières.
Un pattern simple est les tests pilotés par table : bouclez sur une table de fixtures et vérifiez à la fois le statut et la raison pour chaque cas. Cela garde le fichier de test court tout en couvrant beaucoup de scénarios.
Les tests de contrat répondent à une autre question : votre module de validation renvoie-t-il toujours la même structure ? Si une équipe attend { status, reason, normalized } et qu'une autre version change silencieusement, vous obtenez des bugs qui semblent aléatoires.
Les tests de contrat doivent vérifier des choses comme :
status, reason, normalized_email)valid, invalid, risky)Les tests basés sur les propriétés peuvent aider aussi. Au lieu d'écrire à la main chaque faute, générez des quasi-erreurs : espaces en plus, caractères échangés autour du @, points répétés ou domaines en casse mixte. Le but est d'attraper des bugs du parseur et des cas limites que vous n'aviez pas pensés inclure.
Les tests par instantané peuvent aider pour les messages destinés à l'UI, mais utilisez-les prudemment. Préférez snapshotter des codes d'erreur stables, pas du texte complet. Si vous devez verrouiller des messages, gardez-les courts et consistants pour que de petites modifications de copie ne cassent pas la moitié de la suite.
Les tests d'intégration sont souvent où la logique de validation d'e-mails devient peu fiable. Dès qu'un test dépend de DNS réel, de recherches MX ou d'un service tiers, vous pouvez rencontrer des échecs aléatoires qui n'ont rien à voir avec votre code.
Pour les runs CI quotidiens, visez des tests rapides et reproductibles. Traitez le réseau comme une entrée que vous contrôlez.
Une approche pratique est de simuler la frontière réseau et de vous concentrer sur ce que fait votre application avec chaque résultat. Si votre service d'inscription appelle une API de validation d'e-mails, remplacez cet appel par un stub qui renvoie des réponses connues pour vos fixtures. Vous testez toujours le flux complet (contrôleur, service, décision de politique, message d'erreur), mais vous ne testez pas Internet.
Les patterns qui fonctionnent bien :
Les timeouts méritent une attention particulière. Décidez de votre politique et testez-la explicitement : quand la validation expire, traitez-vous l'e-mail comme invalide ou comme inconnu et laissez-vous l'utilisateur poursuivre avec une vérification supplémentaire ? Les deux peuvent être corrects, mais seulement si c'est cohérent.
Les tests simulés peuvent dériver par rapport à la réalité. Un domaine qui avait des enregistrements MX peut devenir mort. Une classification jetable peut changer. Pour détecter la dérive, conservez un job séparé qui effectue des appels réseau réels, mais ne l'exécutez pas à chaque pull request.
Une configuration typique est un run nocturne ou un workflow live manuel. Gardez cette voie petite : une poignée d'e-mails représentatifs par catégorie (fautes, domaines morts, catch-all, jetables) suffit pour détecter la dérive sans créer du bruit.
Si vous utilisez Verimail en production, vos tests CI peuvent simuler sa réponse disposable: true pour une fixture comme [email protected], et vous vérifiez que votre UI bloque l'inscription et affiche le bon message. Ensuite, votre job nocturne peut interroger l'API réelle avec un petit ensemble contrôlé d'adresses et vous alerter si les résultats changent, afin que vous puissiez mettre à jour les fixtures ou ajuster la politique avant que les utilisateurs ne le remarquent.
Une suite de tests peut sembler chargée et quand même manquer les échecs qui vous nuisent en production. Le risque majeur est la fausse confiance : les tests passent, mais les vrais utilisateurs sont bloqués, ou des inscriptions frauduleuses passent à travers.
Un piège fréquent est de traiter les domaines catch-all comme la preuve que la boîte existe. Catch-all signifie seulement que le domaine accepte le courrier pour n'importe quelle adresse. Si votre logique approuve automatiquement tout sur un catch-all, vos fixtures vous entraînent en silence à accepter des adresses pourries.
Autre piège : utiliser seulement une regex et l'appeler validation. La regex attrape des problèmes de format évidents, mais elle ne vous dit pas si le domaine existe, s'il a des MX, ou si l'adresse vient d'un fournisseur jetable. Si vos tests ne couvrent que des motifs de chaînes, vous testez votre regex, pas le comportement e-mail.
Bloquer fermement les inscriptions sur des problèmes DNS temporaires est aussi une erreur fréquente. Les réseaux réels ont des timeouts et des pannes intermittentes. Si vos tests ne couvrent que DNS OK et DNS FAIL, vous risquez de rejeter de bons utilisateurs lors d'une courte panne. Une meilleure approche est de traiter certains erreurs comme inconnues et de réessayer, ou d'autoriser l'inscription mais de marquer l'adresse pour vérification ultérieure.
Les domaines internationalisés et les règles modernes des boîtes sont faciles à oublier. Le plus-addressing (comme [email protected]) est valide et largement utilisé. Certains utilisateurs ont aussi des domaines non-ASCII. Si vos fixtures n'incluent jamais ces cas, vous livrerez un validateur qui casse des entrées normales.
Quelques façons dont les équipes se trompent :
+ est invalide ou le supprimer incorrectement.Une sauvegarde pratique est de séparer les résultats en compartiments clairs (invalid, risky, unknown, valid) et de tester les transitions entre eux. Des outils comme Verimail aident en renvoyant des signaux structurés (syntax, domain, MX, disposable), ce qui facilite les assertions sans deviner à partir d'un simple pass/fail.
Avant de merger, faites un petit passage pour la couverture et la confiance. Le but n'est pas de tester chaque e-mail sur terre, mais de s'assurer que les fixtures se comportent comme de vraies inscriptions et échouent de façon prévisible.
Parcourez votre ensemble de fixtures par catégorie. Si une catégorie n'a qu'un ou deux exemples, un petit changement de code peut casser de vrais utilisateurs et vos tests resteront verts. Visez un petit groupe de cas par catégorie afin d'attraper les bords (comme une faute qui semble toujours valide).
Utilisez cette checklist courte :
Ne négligez pas la stabilité. Les tests qui dépendent de domaines réels peuvent pourrir avec le temps, et le comportement catch-all peut changer sans prévenir. Pour les tests unitaires, préférez des fixtures qui n'exigent pas le réseau et conservez le comportement des domaines comme des réponses simulées. Pour les tests d'intégration, limitez l'étendue et rendez-les intentionnels. Un pattern simple est un petit job nocturne qui exerce quelques cas connus contre votre service de validation (ou fournisseur), tandis que le CI reste concentré sur des tests unitaires déterministes.
Un problème SaaS courant : votre formulaire d'inscription semble correct en test manuel, mais une fois lancé vous recevez des vagues d'inscriptions spam. Beaucoup utilisent des fournisseurs jetables, ce qui signifie de mauvais leads, des taux de rebond plus élevés et une base d'utilisateurs plus sale.
Une approche pratique est de définir un comportement clair et de le verrouiller avec des fixtures. Par exemple : bloquer les adresses jetables, rejeter les domaines morts et autoriser (mais avertir) les domaines catch-all.
Voici un petit ensemble de fixtures qui couvre des entrées d'inscription réalistes sans dépendre de chaînes aléatoires :
[email protected] (semble réel, mais le domaine est mal orthographié)[email protected] (utilisez .invalid pour représenter un domaine qui ne doit jamais se résoudre)[email protected] (traitez-le comme catch-all dans les mocks, pas comme un fait DNS réel)[email protected] (traitez-le comme jetable dans les mocks, pas comme un fournisseur réel)[email protected] (une adresse normale pour le chemin heureux)L'important est que votre app ne devrait pas tenter de prouver le statut catch-all ou jetable via l'internet public pendant les tests unitaires. Au lieu de cela, votre couche de validation devrait renvoyer un résultat normalisé que la logique d'inscription peut utiliser.
Un ensemble de règles simple pour le endpoint d'inscription pourrait être :
is_disposable = true : bloquer l'inscription avec une erreur clairedomain_status = dead : rejeter et demander une autre adresseis_catch_all = true : autoriser l'inscription, mais afficher un avertissement (et envisager une vérification supplémentaire)Pour garder le CI rapide et prévisible, séparez les tests en deux vitesses : tests unitaires rapides pour votre logique de décision, et tests d'intégration simulés pour la frontière du validateur.
// 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, le test d'intégration simulé peut vérifier que votre code d'inscription appelle le validateur une fois et gère les timeouts et les réponses d'erreur, sans effectuer réellement des recherches DNS ou MX.
Si vous voulez des vérifications réelles (syntax conforme RFC, vérification de domaine, recherche MX et correspondance blocklist/jetable) sans construire et maintenir toute cette logique vous-même, une API de validation d'e-mails peut convenir. Pour les équipes utilisant déjà Verimail, un pattern simple est de garder la plupart des tests simulés et déterministes, puis d'exécuter un petit ensemble de vérifications de contrat pour s'assurer que votre intégration avec verimail.co correspond toujours à la structure de réponse que votre application attend.