Якщо ваші тести електронної пошти ідеально працюють на вашому ноутбуці, але розвалюються, щойно потрапляють у CI, ви не самотні. Зазвичай у відповідь на це в код тесту додають виклики sleep або збільшують кількість спроб (retry count), поки білд не пройде успішно. Це може приглушити шум на день, але це не виправляє баг. Це лише приховує його.
Справжня проблема полягає в тому, як ваш тест ідентифікує, який саме лист потрібно відкрити.
Проблема спільного поштового ящика
На вашій локальній машині ви запускаєте по одному тесту за раз. Приходить один лист. Ви його забираєте. Просто.
CI — це зовсім інше середовище. Один pull request може запустити чотири, вісім або шістнадцять паралельних завдань. Якщо всі вони використовують один спільний тестовий поштовий ящик — будь то сервер Mailosaur, поштовий ящик Mailtrap або реальний акаунт на стейджинг-домені — вони всі одночасно пишуть в один і той самий «бакет». Завдання А надсилає скидання пароля. Завдання B надсилає запрошення. Завдання C повторює невдалий потік вітання (welcome flow). Тим часом фонові воркери та черги доставки додають джиттер (jitter), який ви не можете контролювати.
Коли кожне завдання звертається до цього спільного ящика і запитує найновіший лист із темою «Reset your password», починається гонка. Тест, який виграв, отримує правильний лист. Тест, який програв, клікає на посилання, призначене для іншого завдання, виконує перевірки (assertions) проти невірного контенту і завершується помилкою, яка виглядає як проблема з таймінгом. Але це не проблема таймінгу. Це проблема ідентифікації.
Чому підхід «найновіший лист» не працює
У цей крихкий патерн легко потрапити, бо він здається інтуїтивно зрозумілим:
- Запустити сценарій користувача (user flow).
- Опитувати (poll) поштовий ящик кожні кілька секунд.
- Відкрити останній лист, що відповідає темі.
- Клікнути на перше посилання та виконати перевірки.
Ця схема руйнується з кількох причин, окрім простого паралелізму. Повторна спроба з попереднього невдалого запуску може прийти із запізненням і раптово стати найновішим листом саме в той момент, коли ваш поточний тест робить запит. Фонові воркери всередині вашого застосунку можуть поставити два листи в чергу та доставити другий раніше за перший. Самі лише теми листів є слабкими ідентифікаторами; ваш стейджинг-застосунок може надсилати схожі листи з різних шляхів. Сортування за часовою міткою (timestamp) ще гірше, ніж здається, оскільки розсинхронізація годинників (clock skew) між CI-раннером і поштовим провайдером — це реальність, а поштові API часто кешують або групують свої індекси.
Часові мітки стають неточними в навантажених середовищах. Вам потрібно щось пряме.
Що таке run token насправді
Run token — це не що інше, як унікальний рядок, згенерований на початку вашого тесту та вставлений у лист, який надсилає ваш застосунок. Він не обов'язково має бути видимим для користувача або виглядати елегантно. Він лише має гарантувати, що ви зможете довести, що цей конкретний лист належить саме цьому конкретному виконанню тесту.
Найкраще працюють конкретні приклади. Перед початком тесту згенеруйте такий токен, як:
- UUID:
550e8400-e29b-41d4-a716-446655440001 - ID запиту в межах білда:
req_ci_build_4821_a7f3 - Slug запрошення або суфікс метаданих:
signup-token-8k2m9n - Випадковий hex-рядок, згенерований тест-ранером:
test-run-a4f9c2d1
Якщо ви контролюєте код бекенду, передайте токен у контекст листа та відрендеріть його десь у тілі повідомлення. Якщо ви тестуєте застосунок типу «чорна скринька» (black-box), перевірте, чи приймає застосунок вже наявне поле посилання, яке ви можете використати. Якщо ні, іноді можна вставити токен у локальну частину адреси отримувача за допомогою plus addressing — testuser+a4f9c2d1@example.com, хоча це працює лише в тому випадку, якщо ваш застосунок зберігає його та повертає назад у листі.
Суть у тому, щоб припинити шукати за метаданими, якими вже володіє поштова система. Шукайте за даними, якими володіє ваш тест.
Надійний патерн
Замініть алгоритм «найновішого листа» на вузький пошук на основі токена:
- Згенеруйте run token перед тим, як запускати будь-який сценарій.
- Запустіть дію користувача, переконавшись, що застосунок включить токен у вихідний лист.
- Опитуйте поштового провайдера, використовуючи фільтри, обмежені цим токеном. Якщо API підтримує пошук по тілу листа, використовуйте його. Якщо ні — отримайте потенційні повідомлення та виконайте grep їхніх тіл на стороні клієнта.
- Перевірте (assert), що токен існує в тілі повідомлення, перш ніж торкатися будь-яких посилань, кнопок або кодів підтвердження.
- Тільки після цього витягуйте URL підтвердження або код і продовжуйте.
Ця послідовність має значення. Якщо ви спочатку витягнете посилання, а потім перевірите токен, ви вже клікнете на неправильний лист. Перевірка (assertion) — це ваш вартовий.
На практиці ваш хелпер має шукати Subject:"Welcome to AppName" AND Body:"a4f9c2d1" замість Subject:"Welcome to AppName" sort:-received. Багато сервісів тестування пошти надають пошукові API, які підтримують фільтри вмісту тіла листа. Використовуйте їх. Якщо ви працюєте з простішим провайдером, тримайте логіку опитування (polling) в одному місці, щоб ви могли послідовно додавати фільтрацію на стороні клієнта в кожному тесті.
Три правила для забезпечення надійності системи
Токен запуску (run token) фіксує вибір, але вам все одно потрібна дисципліна щодо того, як ви проводите опитування та що робите, коли щось іде не так.
Логуйте стан поштової скриньки у разі помилки. Коли тест завершується невдало, виводьте ідентифікатор скриньки, тему запиту, точний часовий проміжок і кількість повідомлень, що відповідають вашим критеріям. Це перетворює розпливчасту помилку "email not found" на конкретну історію. Якщо завдання 7823 підхопило повідомлення про повторну спробу від завдання 7821, тому що воно прийшло на три секунди пізніше, ваші логи мають це чітко відображати. Без цього контексту ви будете звинувачувати таймінги та додавати ще один sleep.
Тримайте всю логіку опитування пошти в одному файлі-хелпері. Не розкидайте виклики setTimeout та cy.task по двадцяти тестових файлах. Централізуйте логіку очікування повідомлень, повторних викликів API та застосування backoff. Якщо кожен тест використовує той самий хелпер, ваші правила фільтрації залишатимуться послідовними, а коли ви покращите логіку пошуку, це принесе користь усім тестам. Це також полегшує контроль перевірки токена: якщо хелпер вимагає аргумент token, ніхто випадково не скористається "милицею" у вигляді пошуку "останнього повідомлення".
Стежте за повторними спробами (retries). Повторні спроби тестів є звичним явищем у CI, але кожна спроба створює ще один лист у скриньці. Якщо тест проходить з третьої спроби, ви можете відсвяткувати й забути про це. Але ви можете пропустити те, що перша та друга спроби виявили реальний баг — стан гонитви (race condition), дублювання відправки або відсутність індексу — який були замасковані зайвими повідомленнями. Якщо вам необхідно використовувати повторні спроби, перевіряйте, чи не містить скринька неочікуваних дублікатів після помилки. Ще краще — розгляньте можливість очищення скриньки або використання унікальної адреси для кожного завдання, якщо ваш провайдер підтримує динамічні скриньки. Повторні спроби не повинні ставати стратегією для маскування ненадійної логіки вибору.
Головний висновок
Сортування поштової скриньки за датою та отримання першого результату — це не тестування. Це вгадування, замасковане під код. Токен запуску коштує майже нічого — одна строкова змінна, один додатковий параметр фільтрації, можливо, невелика зміна шаблону — і він надає вашому тесту детерміновану ідентичність. Він доводить, що повідомлення перед вами належить саме до того запуску, який ви виконуєте зараз.
Припиніть додавати sleep і сподіватися, що мережа працюватиме стабільно. Генеруйте токен, вставляйте його в електронний лист і шукайте безпосередньо за ним. Ваші запуски в CI стануть швидшими, логи — зрозумілішими, і ви нарешті зможете довіряти тому, що повідомляє вам набір тестів для роботи з поштою.
