Se i tuoi test email funzionano perfettamente sul tuo laptop ma falliscono non appena arrivano in CI, non sei solo. La risposta abituale è quella di disseminare chiamate sleep nel codice di test o aumentare il numero di tentativi (retry) finché la build non passa. Questo potrebbe ridurre il rumore per un giorno, ma non risolve il bug. Lo nasconde soltanto.

Il vero problema è il modo in cui il tuo test identifica quale email aprire.

Il problema della casella di posta condivisa

Sulla tua macchina locale, esegui un test alla volta. Arriva un'email. La prendi. Semplice.

La CI è un ambiente completamente diverso. Una singola pull request potrebbe attivare quattro, otto o sedici job paralleli. Se condividono tutti una casella di posta di test — che si tratti di un server Mailosaur, di una casella Mailtrap o di un account reale su un dominio di staging — stanno tutti scrivendo nello stesso contenitore contemporaneamente. Il Job A invia un reset della password. Il Job B invia un invito. Il Job C riprova un flusso di benvenuto fallito. Nel frattempo, i worker in background e le code di consegna aggiungono un jitter che non puoi controllare.

Quando ogni job attinge a quella casella condivisa e richiede il messaggio più recente con l'oggetto "Reset your password", si scatena una gara. Il test che vince ottiene l'email corretta. Il test che perde clicca su un link destinato a un altro job, esegue l'assertion sul contenuto sbagliato e fallisce con un errore che sembra un problema di tempistica. Non è un problema di tempistica. È un problema di identità.

Perché il metodo "Messaggio più recente" fallisce

Questo schema fragile è facile da adottare perché sembra intuitivo:

  1. Attiva il flusso utente.
  2. Interroga la casella di posta ogni pochi secondi.
  3. Apri il messaggio più recente che corrisponde all'oggetto.
  4. Clicca sul primo link ed esegui le assertion.

Questo approccio crolla per diverse ragioni che vanno oltre il semplice parallelismo. Un tentativo di ripristino da un'esecuzione precedente fallita può arrivare in ritardo, diventando improvvisamente il messaggio più recente proprio mentre il tuo test attuale interroga la casella. I worker in background all'interno della tua applicazione potrebbero mettere in coda due email e consegnare la seconda prima della prima. L'oggetto da solo è un identificatore debole; la tua applicazione di staging potrebbe inviare email simili da percorsi diversi. Ordinare per timestamp è peggio di quanto sembri, perché lo scarto temporale (clock skew) tra il runner della CI e il provider di posta è un fenomeno reale, e le API di posta spesso mettono in cache o raggruppano i propri indici.

I timestamp diventano imprecisi in ambienti trafficati. Hai bisogno di qualcosa di diretto.

Cos'è realmente un Run Token

Un run token non è altro che una stringa univoca generata all'inizio del test e iniettata nell'email inviata dalla tua applicazione. Non deve essere visibile all'utente e non deve apparire elegante. Deve solo garantire che tu possa dimostrare che questo specifico messaggio appartiene a questa specifica esecuzione del test.

Gli esempi concreti sono i migliori. Prima che il test inizi, genera un token come:

  • Un UUID: 550e8400-e29b-41d4-a716-446655440001
  • Un ID richiesta limitato alla build: req_ci_build_4821_a7f3
  • Uno slug di invito o un suffisso

In pratica, il tuo helper dovrebbe cercare Subject:"Welcome to AppName" AND Body:"a4f9c2d1" invece di Subject:"Welcome to AppName" sort:-received. Molti servizi di test delle email espongono API di ricerca che accettano filtri per il contenuto del corpo del messaggio. Usali. Se stai lavorando con un provider più semplice, tieni la logica di polling in un unico posto, così da poter aggiungere il filtraggio lato client in modo coerente in ogni test.

Tre regole per mantenere l'integrità del sistema

Un run token fissa la selezione, ma è comunque necessaria disciplina su come effettuare il polling e su cosa fare quando qualcosa va storto.

Registra lo stato della inbox in caso di errore. Quando un test fallisce, restituisci l'identificativo della inbox, l'oggetto della query, l'intervallo temporale esatto e quanti messaggi hanno soddisfatto i tuoi criteri. Questo trasforma un vago errore "email non trovata" in una spiegazione concreta. Se il job 7823 ha prelevato un messaggio di retry dal job 7821 perché è arrivato tre secondi dopo, i tuoi log dovrebbero renderlo evidente. Senza questo contesto, darai la colpa al timing e aggiungerai un altro sleep.

Mantieni tutto il polling delle email in un unico file helper. Non disperdere le chiamate setTimeout e cy.task in venti file di test diversi. Centralizza la logica che attende i messaggi, riprova la chiamata API e applica il backoff. Se ogni test utilizza lo stesso helper, le regole di filtraggio rimarranno coerenti e, quando migliorerai la logica di ricerca, ogni test ne beneficerà. Questo rende anche più facile imporre il controllo del token; se l'helper richiede un argomento token, nessuno potrà accidentalmente ricorrere alla "stampella" dell'ultimo messaggio ricevuto.

Monitora i tuoi retry. I tentativi di riprova (retry) dei test sono comuni nella CI, ma ogni tentativo crea un'altra email nella inbox. Se il test passa al terzo tentativo, potresti festeggiare e andare avanti. Ciò che ti sfugge è che i primi due tentativi hanno esposto un bug reale — una race condition, un invio duplicato o un indice mancante — che i messaggi extra hanno mascherato. Se devi usare i retry, controlla se la inbox contiene duplicati inaspettati dopo un fallimento. Meglio ancora, considera di pulire la inbox o di utilizzare un indirizzo univoco per ogni job, se il tuo provider supporta le inbox dinamiche. I retry non dovrebbero diventare una strategia per compensare una logica di selezione inaffidabile.

La lezione fondamentale

Ordinare una inbox per data e prendere il primo risultato non è testing. È tirare a indovinare travestito da codice. Un run token non costa quasi nulla — una variabile stringa, un parametro di filtro extra, forse una piccola modifica al template — e conferisce al tuo test un'identità deterministica. Dimostra che il messaggio che hai davanti appartiene all'esecuzione che stai effettuando proprio ora.

Smetti di aggiungere sleep sperando che la rete si comporti bene. Genera un token, inseriscilo nell'email e cercalo direttamente. Le tue esecuzioni CI saranno più veloci, i tuoi log saranno leggibili e finalmente ti fiderai di ciò che la suite di test delle email ti sta dicendo.