Jeśli Twoje testy e-mailowe działają idealnie na laptopie, a sypią się w momencie trafienia do CI, nie jesteś sam. Zwykłą reakcją jest dodawanie wywołań sleep w kodzie testowym lub zwiększanie liczby prób (retry count), aż build przejdzie. To może uciszyć szum na jeden dzień, ale nie naprawia błędu. To tylko go maskuje.
Prawdziwym problemem jest to, jak Twój test identyfikuje, który e-mail ma otworzyć.
Problem współdzielonej skrzynki odbiorczej
Na lokalnej maszynie uruchamiasz jeden test naraz. Przychodzi jeden e-mail. Pobierasz go. Proste.
CI to zupełnie inne środowisko. Pojedynczy pull request może uruchomić cztery, osiem lub szesnaście równoległych zadań. Jeśli wszystkie korzystają ze wspólnej skrzynki testowej — czy to serwera Mailosaur, skrzynki Mailtrap, czy prawdziwego konta na domenie stagingowej — wszystkie zapisują dane do tego samego pojemnika w tym samym czasie. Zadanie A wysyła reset hasła. Zadanie B wysyła zaproszenie. Zadanie C ponawia nieudaną ścieżkę powitalną (welcome flow). W międzyczasie procesy w tle i kolejki dostarczania wprowadzają jitter, którego nie możesz kontrolować.
Kiedy każde zadanie zagląda do tej wspólnej skrzynki i pyta o najnowszą wiadomość o temacie „Resetuj swoje hasło”, zaczyna się wyścig. Test, który wygra, otrzymuje właściwy e-mail. Test, który przegra, klika w link przeznaczony dla innego zadania, sprawdza błędną treść i kończy się błędem, który wygląda jak problem z czasem (timing problem). To nie jest problem z czasem. To problem z tożsamością.
Dlaczego metoda „najnowszej wiadomości” zawodzi
Łatwo wpaść w ten nietrwały wzorzec, ponieważ wydaje się on intuicyjny:
- Uruchom ścieżkę użytkownika.
- Sprawdzaj skrzynkę co kilka sekund.
- Otwórz najnowszą wiadomość pasującą do tematu.
- Kliknij pierwszy link i wykonaj asercje.
To rozpada się z kilku powodów wykraczających poza zwykły paralelizmu. Ponowienie (retry) z poprzedniego nieudanego przebiegu może dotrzeć z opóźnieniem, nagle stając się najnowszą wiadomością dokładnie w momencie, gdy Twój obecny test sprawdza skrzynkę. Procesy w tle wewnątrz Twojej aplikacji mogą kolejkowć dwa e-maile i dostarczyć drugi przed pierwszym. Same tematy wiadomości to słabe identyfikatory; Twoja aplikacja stagingowa może wysyłać podobne e-maile z różnych ścieżek. Sortowanie według znacznika czasu (timestamp) jest gorsze, niż się wydaje, ponieważ przesunięcia zegara (clock skew) między runnerem CI a dostawcą poczty są realne, a API pocztowe często buforują lub grupują swoje indeksy.
Znaczniki czasu stają się nieprecyzyjne w intensywnych środowiskach. Potrzebujesz czegoś bezpośredniego.
Czym właściwie jest token przebiegu
Token przebiegu (run token) to nic innego jak unikalny ciąg znaków generowany na początku testu i wstrzykiwany do e-maila wysyłanego przez Twoją aplikację. Nie musi być widoczny dla użytkownika ani wyglądać elegancko. Musi jedynie gwarantować, że możesz udowodnić, iż ta konkretna wiadomość należy do tego konkretnego wykonania testu.
Najlepiej działają konkretne przykłady. Przed rozpoczęciem testu wygeneruj token, taki jak:
- UUID:
550e8400-e29b-41d4-a716-446655440001 - ID żądania w zakresie buildu:
req_ci_build_4821_a7f3 - Slug zaproszenia lub przyrostek metadanych:
signup-token-8k2m9n - Losowy ciąg szesnastkowy generowany przez runnera testów:
test-run-a4f9c2d1
Jeśli masz kontrolę nad kodem backendu, przekaż token do kontekstu e-maila i wyrenderuj go w treści. Jeśli testujesz aplikację typu black-box, sprawdź, czy aplikacja nie przyjmuje już pola referencyjnego, które możesz przejąć. Jeśli nie, możesz czasem osadzić token w części lokalnej adresu odbiorcy, używając adresowania z plusem — testuser+a4f9c2d1@example.com — choć działa to tylko wtedy, gdy Twoja aplikacja zachowuje go i odsyła w e-mailu.
Chodzi o to, aby przestać dopasowywać na podstawie metadanych, którymi zarządza już system pocztowy. Dopasowuj na podstawie danych, którymi zarządza Twój test.
Niezawodny wzorzec
Zastąp algorytm „najnowszej wiadomości” wąskim wyszukiwaniem opartym na tokenie:
- Wygeneruj token przebiegu przed uruchomieniem jakiejkolwiek ścieżki.
- Rozpocznij akcję użytkownika, upewniając się, że aplikacja dołączy token do wychodzącego e-maila.
- Sprawdzaj dostawcę poczty za pomocą filtrów ograniczonych do tego tokenu. Jeśli API obsługuje wyszukiwanie w treści, użyj go. Jeśli nie, pobierz wiadomości-kandydatki i przefiltgruj ich treści (grep) po stronie klienta.
- Potwierdź (assert), że token istnieje w treści wiadomości, zanim dotkniesz jakichkolwiek linków, przycisków lub kodów weryfikacyjnych.
- Dopiero wtedy wyodrębnij URL potwierdzenia lub kod i kontynuuj.
Ta kolejność ma znaczenie. Jeśli najpierw wyodrębnisz link, a dopiero potem sprawdzisz token, kliknąłeś już niewłaściwy e-mail. Asercja jest Twoim strażnikiem.
W praktyce Twój helper powinien szukać Subject:"Welcome to AppName" AND Body:"a4f9c2d1" zamiast Subject:"Welcome to AppName" sort:-received. Wiele usług testowania poczty udostępnia API wyszukiwania, które akceptuje filtry treści wiadomości. Korzystaj z nich. Jeśli pracujesz z prostszym dostawcą, trzymaj logikę odpytywania (polling) w jednym miejscu, aby móc spójnie dodawać filtrowanie po stronie klienta w każdym teście.
Trzy zasady, aby zachować rzetelność systemu
Token uruchomienia (run token) unieruchamia wybór, ale wciąż potrzebujesz dyscypliny w kwestii tego, jak odpytujesz system i co robisz, gdy coś pójdzie nie tak.
Loguj stan skrzynki odbiorczej w przypadku błędu. Gdy test zawiedzie, wypisz identyfikator skrzynki, linię tematu, o którą pytałeś, dokładne okno czasowe oraz liczbę wiadomości spełniających Twoje kryteria. To zamienia niejasny błąd „email not found” w konkretną historię. Jeśli zadanie 7823 pobrało wiadomość ponowienia (retry) z zadania 7821, ponieważ dotarła ona trzy sekundy później, Twoje logi powinny to jasno wskazywać. Bez tego kontekstu będziesz obwiniać opóźnienia czasowe i dodawać kolejne instrukcje sleep.
Trzymaj całe odpytywanie poczty w jednym pliku helpera. Nie rozrzucaj wywołań setTimeout i cy.task po dwudziestu plikach testowych. Scentralizuj logikę, która czeka na wiadomości, ponawia wywołania API i stosuje mechanizm backoff. Jeśli każdy test korzysta z tego samego helpera, reguły filtrowania pozostają spójne, a każda poprawa logiki wyszukiwania przynosi korzyść wszystkim testom. Ułatwia to również wymuszanie sprawdzania tokena; jeśli helper wymaga argumentu w postaci tokena, nikt nie będzie mógł przypadkowo wrócić do „podpórki” w postaci „ostatniej wiadomości”.
Uważaj na ponowienia (retries). Ponawianie testów jest powszechne w CI, ale każde ponowienie tworzy kolejną wiadomość w skrzynce odbiorczej. Jeśli test przejdzie za trzecią próbą, możesz świętować i iść dalej. To, co umyka, to fakt, że próby pierwsza i druga ujawniły prawdziwy błąd — race condition, duplikację wysyłki lub brakujący indeks — który dodatkowe wiadomości zamaskowały. Jeśli musisz korzystać z ponowień, sprawdź, czy po błędzie w skrzynce nie ma nieoczekiwanych duplikatów. Jeszcze lepiej: rozważ czyszczenie skrzynki lub używanie unikalnego adresu dla każdego zadania, jeśli Twój dostawca obsługuje dynamiczne skrzynki odbiorcze. Ponowienia nie powinny stać się strategią maskowania zawodnej logiki wyboru.
Najważniejszy wniosek
Sortowanie skrzynki według daty i branie pierwszego wyniku to nie jest testowanie. To zgadywanie przebrane za kod. Token uruchomienia kosztuje prawie nic — jedna zmienna typu string, jeden dodatkowy parametr filtra, być może mała zmiana w szablonie — a nadaje Twojemu testowi deterministyczną tożsamość. Dowodzi, że wiadomość, którą widzisz, należy do uruchomienia, które wykonujesz właśnie teraz.
Przestań dodawać sleep i liczyć na to, że sieć będzie współpracować. Wygeneruj token, umieść go w e-mailu i szukaj go bezpośrednio. Twoje przebiegi CI będą szybsze, logi czytelne, a Ty w końcu zaczniesz ufać temu, co mówi Ci zestaw testów e-mailowych.
