اگر تست‌های ایمیل شما روی لپ‌تاپتان بی‌نقص اجرا می‌شوند اما به محض رسیدن به CI از هم می‌پاشند، تنها نیستید. واکنش معمول این است که دستورات sleep را در کد تست پخش کنید یا تعداد تلاش مجدد (retry count) را بالا ببرید تا بیلد با موفقیت انجام شود. این کار ممکن است برای یک روز سر و صدا را کم کند، اما باگ را حل نمی‌کند؛ فقط آن را پنهان می‌کند.

مشکل اصلی این است که تست شما چگونه تشخیص می‌دهد کدام ایمیل را باز کند.

مشکل اینباکس مشترک

در سیستم محلی خود، هر بار فقط یک تست را اجرا می‌کنید. یک ایمیل می‌رسد. آن را برمی‌دارید. ساده است.

CI محیط کاملاً متفاوتی است. یک Pull Request واحد ممکن است چهار، هشت یا شانزده کار موازی (parallel jobs) را فعال کند. اگر همه آن‌ها از یک اینباکس تست مشترک استفاده کنند — خواه سرور Mailosaur باشد، یا اینباکس Mailtrap، یا یک حساب واقعی در یک دامنه استیجینگ (staging domain) — همگی همزمان در حال نوشتن در یک ظرف (bucket) واحد هستند. کار A یک بازیابی رمز عبور می‌فرستد. کار B یک دعوت‌نامه می‌فرستد. کار C یک جریان خوش‌آمدگویی شکست‌خورده را دوباره امتحان می‌کند. در همین حال، ورکر‌های پس‌زمینه (background workers) و صف‌های ارسال، نوساناتی (jitter) ایجاد می‌کنند که نمی‌توانید کنترلشان کنید.

وقتی هر کار به آن اینباکس مشترک مراجعه می‌کند و جدیدترین پیام با موضوع "Reset your password" را درخواست می‌کند، یک مسابقه (race) شکل می‌گیرد. تستی که برنده می‌شود، ایمیل درست را دریافت می‌کند. تستی که بازنده می‌شود، روی لینکی کلیک می‌کند که برای کار دیگری در نظر گرفته شده بود، محتوای اشتباه را بررسی (assert) می‌کند و با خطایی مواجه می‌شود که شبیه به یک مشکل زمان‌بندی (timing problem) به نظر می‌رسد. این مشکل زمان‌بندی نیست؛ مشکل هویت (identity) است.

چرا روش «جدیدترین پیام» شکست می‌خورد

این الگوی شکننده به راحتی اتفاق می‌افتد چون بصری و منطقی به نظر می‌رسد:

  1. جریان کاربر (user flow) را فعال کنید.
  2. هر چند ثانیه یک‌بار اینباکس را بررسی (poll) کنید.
  3. جدیدترین پیامی را که با خط موضوع مطابقت دارد باز کنید.
  4. روی اولین لینک کلیک کرده و بررسی‌ها (assertions) را انجام دهید.

این روش به دلایل متعددی فراتر از موازی‌سازی ساده، از هم می‌پاشد. یک تلاش مجدد از اجرای ناموفق قبلی ممکن است با تأخیر برسد و درست زمانی که تست فعلی شما در حال بررسی است، ناگهان به جدیدترین پیام تبدیل شود. ورکر‌های پس‌زمینه در اپلیکیشن شما ممکن است دو ایمیل را در صف قرار دهند و دومی را قبل از اولی تحویل دهند. خطوط موضوع به تنهایی شناسه‌های ضعیفی هستند؛ اپلیکیشن استیجینگ شما ممکن است ایمیل‌های مشابهی را از مسیرهای مختلف ارسال کند. مرتب‌سازی بر اساس برچسب زمانی (timestamp) بدتر از آن چیزی است که به نظر می‌رسد، زیرا اختلاف ساعت (clock skew) بین runnerِ CI و ارائه‌دهنده ایمیل یک واقعیت است و APIهای ایمیل اغلب ایندکس‌های خود را کش (cache) یا دسته‌ای (batch) می‌کنند.

در محیط‌های شلوغ، برچسب‌های زمانی مبهم می‌شوند. شما به چیزی مستقیم نیاز دارید.

توکن اجرا (Run Token) واقعاً چیست

توکن اجرا چیزی نیست جز یک رشته منحصر‌به‌فرد که در شروع تست شما تولید شده و به ایمیلی که اپلیکیشن شما ارسال می‌کند تزریق می‌شود. نیازی نیست که کاربر آن را ببیند و نیازی نیست که ظاهری زیبا داشته باشد. فقط باید تضمین کند که می‌توانید ثابت کنید این پیام خاص متعلق به این اجرای خاصِ تست است.

مثال‌های ملموس بهترین عملکرد را دارند. قبل از شروع تست، توکنی مانند این‌ها تولید کنید:

  • یک UUID: 550e8400-e29b-41d4-a716-446655440001
  • یک ID درخواست در محدوده بیلد: req_ci_build_4821_a7f3
  • یک اسلاگ دعوت‌نامه یا پسوند متادیتا: signup-token-8k2m9n
  • یک رشته هگز تصادفی تولید شده توسط تست‌رانر: test-run-a4f9c2d1

اگر کد بک‌اند را در اختیار دارید، توکن را به context ایمیل پاس دهید و آن را جایی در بدنه (body) رندر کنید. اگر در حال تست یک اپلیکیشن جعبه‌سیاه (black-box) هستید، بررسی کنید که آیا اپلیکیشن از قبل یک فیلد مرجع (reference field) می‌پذیرد که بتوانید از آن استفاده کنید یا خیر. اگر نه، گاهی اوقات می‌توانید توکن را با استفاده از قابلیت plus addressing در بخش local-part گیرنده قرار دهید — مانند testuser+a4f9c2d1@example.com — هرچند این روش تنها زمانی کار می‌کند که اپلیکیشن شما آن را حفظ کرده و در ایمیل بازگرداند (echo).

نکته این است که از تطبیق دادن بر اساس متادیتاهایی که سیستم ایمیل از قبل مالک آن‌هاست، دست بردارید. بر اساس داده‌هایی تطبیق دهید که تست شما مالک آن‌هاست.

الگوی قابل اعتماد

الگوریتم «جدیدترین پیام» را با یک جستجوی محدود و مبتنی بر توکن جایگزین کنید:

  1. قبل از فعال کردن هر جریانی، توکن اجرا را تولید کنید.
  2. اکشن کاربر را شروع کنید و مطمئن شوید که اپلیکیشن توکن را در ایمیل خروجی قرار می‌دهد.
  3. ارائه‌دهنده ایمیل را با فیلترهایی محدود به آن توکن بررسی (poll) کنید. اگر API از جستجو در بدنه پشتیبانی می‌کند، از آن استفاده کنید. در غیر این صورت، پیام‌های کاندید را دریافت کرده و بدنه آن‌ها را در سمت کلاینت با دستور grep جستجو کنید.
  4. قبل از دست زدن به هرگونه لینک، دکمه یا کد تأیید، تأیید (assert) کنید که توکن در بدنه پیام وجود دارد.
  5. تنها پس از آن، URL یا کد تأیید را استخراج کرده و ادامه دهید.

رعایت این ترتیب بسیار مهم است. اگر ابتدا لینک را استخراج کنید و سپس توکن را بررسی کنید، یعنی قبلاً روی ایمیل اشتباه کلیک کرده‌اید. این assert (تأییدیه) در واقع نگهبان شماست.

در عمل، تابع کمکی (helper) شما باید به جای Subject:"Welcome to AppName" sort:-received به دنبال Subject:"Welcome to AppName" AND Body:"a4f9c2d1" بگردد. بسیاری از سرویس‌های تست ایمیل، APIهای جستجویی را ارائه می‌دهند که فیلترهای محتوای بدنه (body) را می‌پذیرند. از آن‌ها استفاده کنید. اگر با ارائه‌دهنده ساده‌تری کار می‌کنید، منطق پیمایش (polling) خود را در یک جا نگه دارید تا بتوانید فیلتر کردن سمت کلاینت را به‌طور یکسان در تمام تست‌ها اعمال کنید.

سه قانون برای حفظ صداقت سیستم

یک توکن اجرا (run token) انتخاب را تثبیت می‌کند، اما همچنان به انضباط در نحوه پیمایش و اقدامات خود هنگام بروز خطا نیاز دارید.

وضعیت اینباکس را هنگام شکست ثبت کنید. وقتی یک تست با شکست مواجه می‌شود، شناسه اینباکس، خط موضوعی مورد جستجو، بازه زمانی دقیق و تعداد پیام‌هایی که با معیارهای شما مطابقت داشتند را خروجی بگیرید. این کار یک خطای مبهم «ایمیل یافت نشد» را به یک روایت ملموس تبدیل می‌کند. اگر کار (job) شماره ۷۸۲۳ یک پیام تلاش مجدد (retry) مربوط به کار ۷۸۲۱ را دریافت کرد چون آن پیام با سه ثانیه تأخیر رسیده بود، لاگ‌های شما باید این موضوع را آشکار کنند. بدون این زمینه (context)، شما زمان‌بندی را مقصر می‌دانید و یک دستور sleep دیگر اضافه می‌کنید.

تمام پیمایش ایمیل را در یک فایل کمکی نگه دارید. فراخوانی‌های setTimeout و cy.task را در بیست فایل تست پراکنده نکنید. منطقی را که منتظر پیام‌ها می‌ماند، فراخوانی API را مجدداً تلاش می‌کند و استراتژی عقب‌نشینی (backoff) را اعمال می‌کند، متمرکز کنید. اگر هر تست از همان فایل کمکی استفاده کند، قوانین فیلتر شما یکپارچه باقی می‌ماند و با بهبود منطق جستجو، تمام تست‌ها از آن بهره‌مند می‌شوند. همچنین این کار اعمال بررسی توکن را آسان‌تر می‌کند؛ اگر فایل کمکی به آرگومان توکن نیاز داشته باشد، هیچ‌کس نمی‌تواند به‌طور تصادفی به تکیه‌گاه «آخرین پیام» متوسل شود.

مراقب تلاش‌های مجدد (retries) خود باشید. تلاش مجدد تست‌ها در CI رایج است، اما هر تلاش مجدد یک ایمیل دیگر در اینباکس ایجاد می‌کند. اگر تست شما در تلاش سوم با موفقیت انجام شد، ممکن است جشن بگیرید و از آن عبور کنید. آنچه از دست می‌دهید این است که تلاش‌های اول و دوم یک باگ واقعی را آشکار کرده بودند — یک race condition، ارسال تکراری، یا یک ایندکس مفقود شده — که پیام‌های اضافی آن را پنهان کرده بودند. اگر مجبور به استفاده از تلاش مجدد هستید، بررسی کنید که آیا اینباکس پس از شکست، حاوی پیام‌های تکراری غیرمنتظره است یا خیر. بهتر است اگر ارائه‌دهنده شما از اینباکس‌های پویا پشتیبانی می‌کند، پاکسازی اینباکس یا استفاده از یک آدرس منحصربه‌فرد برای هر کار (job) را در نظر بگیرید. تلاش مجدد نباید به استراتژی‌ای برای پوشاندن منطق انتخاب غیرقابل اعتماد تبدیل شود.

نتیجه‌گیری اصلی

مرتب‌سازی اینباکس بر اساس تاریخ و برداشتن اولین نتیجه، تست کردن نیست؛ بلکه حدس زدن است که در قالب کد پوشانده شده است. یک توکن اجرا تقریباً هیچ هزینه‌ای ندارد — یک متغیر رشته‌ای، یک پارامتر فیلتر اضافی، و شاید یک تغییر کوچک در قالب — و به تست شما هویت قطعی (deterministic) می‌بخشد. این توکن ثابت می‌کند پیامی که مقابل شماست، متعلق به همان اجرایی است که در حال حاضر در حال انجام آن هستید.

از اضافه کردن sleep و امیدواری به رفتار درست شبکه دست بردارید. یک توکن تولید کنید، آن را در ایمیل قرار دهید و مستقیماً به دنبال آن بگردید. اجراهای CI شما سریع‌تر خواهند بود، لاگ‌های شما خوانا خواهند بود و در نهایت به آنچه مجموعه تست ایمیل به شما می‌گوید، اعتماد خواهید کرد.