Si vos tests d'e-mails fonctionnent parfaitement sur votre ordinateur mais s'effondrent dès qu'ils atteignent la CI, vous n'êtes pas seul. La réponse habituelle consiste à parsemer le code de tests d'appels sleep ou à augmenter le nombre de tentatives jusqu'à ce que le build passe. Cela peut calmer le bruit pendant un jour, mais cela ne corrige pas le bug. Cela ne fait que le masquer.
Le véritable problème réside dans la manière dont votre test identifie l'e-mail à ouvrir.
Le problème de la boîte de réception partagée
Sur votre machine locale, vous exécutez un test à la fois. Un e-mail arrive. Vous le récupérez. Simple.
La CI est un environnement totalement différent. Une seule pull request peut déclencher quatre, huit ou seize jobs parallèles. S'ils partagent tous une boîte de réception de test — qu'il s'agisse d'un serveur Mailosaur, d'une boîte Mailtrap ou d'un compte réel sur un domaine de staging — ils écrivent tous dans le même compartiment au même moment. Le job A envoie une réinitialisation de mot de passe. Le job B envoie une invitation. Le job C tente de nouveau un flux de bienvenue échoué. Pendant ce temps, les workers en arrière-plan et les files d'attente de livraison ajoutent une gigue (jitter) que vous ne pouvez pas contrôler.
Lorsque chaque job fouille dans cette boîte partagée pour demander le message le plus récent avec l'objet « Réinitialisez votre mot de passe », cela devient une course. Le test qui gagne récupère le bon e-mail. Le test qui perd clique sur un lien destiné à un autre job, effectue des assertions sur le mauvais contenu et échoue avec une erreur qui ressemble à un problème de timing. Ce n'est pas un problème de timing. C'est un problème d'identité.
Pourquoi la méthode du « message le plus récent » échoue
Ce modèle fragile est facile à adopter car il semble intuitif :
- Déclencher le flux utilisateur.
- Interroger la boîte de réception toutes les quelques secondes.
- Ouvrir le message le plus récent correspondant à la ligne d'objet.
- Cliquer sur le premier lien et exécuter les assertions.
Cela s'effondre pour plusieurs raisons au-delà du simple parallélisme. Une tentative de réexécution d'un run précédent ayant échoué peut arriver tardivement, devenant soudainement le message le plus récent juste au moment où votre test actuel interroge la boîte. Les workers en arrière-plan de votre application peuvent mettre deux e-mails en file d'attente et livrer le second avant le premier. Les lignes d'objet seules sont de faibles identifiants ; votre application de staging peut envoyer des e-mails similaires via des chemins différents. Le tri par horodatage est pire qu'il n'y paraît, car le décalage d'horloge (clock skew) entre le runner CI et le fournisseur de messagerie est réel, et les API de messagerie mettent souvent leurs index en cache ou les traitent par lots.
Les horodatages deviennent flous dans les environnements chargés. Vous avez besoin de quelque chose de direct.
Ce qu'est réellement un run token
Un run token n'est rien de plus qu'une chaîne unique générée au début de votre test et injectée dans l'e-mail que votre application envoie. Il n'a pas besoin d'être visible par l'utilisateur, ni d'être élégant. Il doit seulement garantir que vous pouvez prouver que ce message spécifique appartient à cette exécution de test spécifique.
Les exemples concrets sont les plus parlants. Avant le début du test, générez un token tel que :
- Un UUID :
550e8400-e29b-41d4-a716-446655440001 - Un ID de requête lié au build :
req_ci_build_4821_a7f3 - Un slug d'invitation ou un suffixe de métadonnées :
signup-token-8k2m9n - Une chaîne hexadécimale aléatoire générée par le test runner :
test-run-a4f9c2d1
Si vous contrôlez le code backend, passez le token dans le contexte de l'e-mail et affichez-le quelque part dans le corps du message. Si vous testez une application de type « boîte noire », vérifiez si l'application accepte déjà un champ de référence que vous pouvez détourner. Sinon, vous pouvez parfois intégrer le token dans la partie locale de l'adresse du destinataire en utilisant le « plus addressing » — testuser+a4f9c2d1@example.com — bien que cela ne fonctionne que si votre application conserve et renvoie cette information dans l'e-mail.
L'idée est d'arrêter de faire correspondre des métadonnées qui appartiennent déjà au système de messagerie. Faites correspondre des données qui appartiennent à votre test.
Le modèle fiable
Remplacez l'algorithme du « message le plus récent » par une recherche ciblée pilotée par un token :
- Générez le run token avant de déclencher n'importe quel flux.
- Lancez l'action utilisateur, en vous assurant que l'application inclura le token dans l'e-mail sortant.
- Interrogez le fournisseur de messagerie avec des filtres limités à ce token. Si l'API prend en charge la recherche dans le corps du message, utilisez-la. Sinon, récupérez les messages candidats et effectuez un
grepsur leurs corps côté client. - Vérifiez que le token existe dans le corps du message avant de toucher à des liens, des boutons ou des codes de vérification.
- Ce n'est qu'ensuite que vous extrayez l'URL ou le code de confirmation pour continuer.
Cet ordre est crucial. Si vous extrayez un lien d'abord et vérifiez le token ensuite, vous avez déjà cliqué sur le mauvais e-mail. L'assertion est votre garde-fou.
En pratique, votre helper devrait rechercher Subject:"Welcome to AppName" AND Body:"a4f9c2d1" plutôt que Subject:"Welcome to AppName" sort:-received. De nombreux services de test d'e-mails exposent des API de recherche qui acceptent des filtres sur le corps du message. Utilisez-les. Si vous travaillez avec un fournisseur plus simple, centralisez votre logique de polling afin de pouvoir ajouter un filtrage côté client de manière cohérente dans chaque test.
Trois règles pour maintenir l'intégrité du système
Un jeton d'exécution (run token) fige la sélection, mais vous devez tout de même faire preuve de discipline quant à votre méthode de polling et à votre réaction en cas de problème.
Journalisez l'état de la boîte de réception en cas d'échec. Lorsqu'un test échoue, affichez l'identifiant de la boîte de réception, l'objet de la requête, la fenêtre temporelle exacte et le nombre de messages correspondant à vos critères. Cela transforme une erreur vague de type « e-mail non trouvé » en un scénario concret. Si le job 7823 a récupéré un message de tentative de l'exécution du job 7821 parce qu'il est arrivé trois secondes plus tard, vos logs doivent le rendre évident. Sans ce contexte, vous attribuerez l'erreur au timing et ajouterez un sleep supplémentaire.
Regroupez tout le polling d'e-mails dans un seul fichier helper. Ne dispersez pas les appels setTimeout et cy.task dans vingt fichiers de test différents. Centralisez la logique qui attend les messages, réessaie l'appel API et applique un backoff. Si chaque test utilise le même helper, vos règles de filtrage restent cohérentes et, lorsque vous améliorerez la logique de recherche, tous les tests en bénéficieront. Cela facilite également l'application de la vérification du jeton ; si le helper requiert un argument de jeton, personne ne pourra accidentellement se rabattre sur la béquille du « dernier message reçu ».
Surveillez vos tentatives (retries). Les tentatives de test sont courantes en CI, mais chaque tentative crée un e-mail supplémentaire dans la boîte de réception. Si votre test réussit à la troisième tentative, vous pourriez vous réjouir et passer à la suite. Ce que vous ne voyez pas, c'est que les tentatives une et deux ont révélé un véritable bug — une condition de concurrence (race condition), un envoi en double ou un index manquant — que les messages supplémentaires ont masqué. Si vous devez utiliser des retries, vérifiez si la boîte de réception contient des doublons inattendus après un échec. Mieux encore, envisagez de nettoyer la boîte de réception ou d'utiliser une adresse unique par job si votre fournisseur prend en charge les boîtes de réception dynamiques. Les retries ne doivent pas devenir une stratégie pour masquer une logique de sélection peu fiable.
L'essentiel à retenir
Trier une boîte de réception par date et récupérer le premier résultat n'est pas du test. C'est de la supposition déguisée en code. Un jeton d'exécution ne coûte presque rien — une variable de type chaîne de caractères, un paramètre de filtre supplémentaire, peut-être un léger changement de template — et il donne à votre test une identité déterministe. Il prouve que le message que vous avez sous les yeux appartient bien à l'exécution que vous lancez actuellement.
Arrêtez d'ajouter des sleep en espérant que le réseau se comporte bien. Générez un jeton, insérez-le dans l'e-mail et recherchez-le directement. Vos exécutions CI seront plus rapides, vos logs seront lisibles et vous finirez par avoir confiance en ce que votre suite de tests d'e-mails vous indique.
