همه چیز با یک پیام اسلک شروع میشود. بیلد قرمز است. شما لیست شکستها را بالا و پایین میکنید، اخم میکنید و همان تست را روی لپتاپ خودتان دوباره اجرا میکنید. سبز میشود. دوباره اجرای CI را امتحان میکنید. شاید یک خطای گذرا بوده است. اما شکست دوباره برمیگردد؛ سرسخت و تکرارپذیر روی سرور، اما برای شما نامرئی.
یک تست مرورگری که در CI با شکست مواجه میشود اما در سیستم محلی پاس میشود، چیزی فراتر از یک مزاحمت است. این موضوع باعث بیاعتمادی میشود. تیمها شروع به مقصر دانستن زمانبندی (timing) میکنند. آنها اصلاحات موقتی اعمال میکنند که هرگز حذف نمیشوند. یک setTimeout اینجا، یک .wait(5000) آنجا. مجموعه تستها کند میشود. شکستها مدام بازمیگردند. آن تستهای ناپایدار (flaky) به بخشهای دائمی تبدیل میشوند و در نهایت، همه شروع میکنند به اینکه یک خط لوله (pipeline) قرمز را به عنوان یک نویز پسزمینه در نظر بگیرند.
این خطرناک است. شما مجموعهای از تستها را نمیخواهید که مدام هشدارهای کاذب بدهند.
CI خراب نیست؛ فقط متفاوت است
محیطهای CI تصادفی نیستند. آنها قطعی (deterministic) هستند. مشکل اینجاست که آنها نسبت به سیستمی قطعی هستند که مکبوک یا ایستگاه کاری لینوکس شما نیست. تنظیمات محلی شما تفاوتهایی را پنهان میکند که یک runner تمیز در CI بلافاصله آنها را آشکار میکند.
به این فکر کنید که چه تعداد از اجزای متحرک با هم تفاوت دارند. ماشین محلی شما ممکن است یک سرور توسعه را با قابلیت hot module reloading اجرا کند، در حالی که CI یک آرتیفکت تولید (production artifact) را با tree shaking و minification میسازد. همین موضوع به تنهایی میتواند مسیرهای کد را حذف کند یا ترتیب اجرا را تغییر دهد. درختهای وابستگی (dependency trees) تغییر میکنند. یک فایل قفل (lockfile) که کاملاً مشابه به نظر میرسد، اگر نسخه مدیریت بسته (package manager) حتی یک نسخه فرعی متفاوت باشد، میتواند نتایج متفاوتی داشته باشد. توالیهای شبکه تغییر میکنند. وای-فای دفتر کار شما ممکن است یک API مرحلهای (staging) را در یک مرحله حل کند؛ اما runner در CI ممکن است به کلاستر متفاوتی پشت یک لود بالانسر برخورد کند و تاخیری (latency) ایجاد کند که شما هرگز نمیبینید.
خود مرورگرها نیز در محیطهای مختلف رفتار متفاوتی دارند. کروم محلی شما دارای افزونهها، اعتبارنامههای کششده، ذخیرهسازی محلی دائمی و یک GPU با شتاب سختافزاری است. CI در هر اجرا از یک پروفایل خالی شروع میشود. چرخه حیات مرورگرها متفاوت است. مسیرهای رندرینگ متفاوت است. فونتهایی که در سیستم شما وجود دارند، در CI جایگزین میشوند. اندازه viewport و نسبت پیکسل دستگاه متفاوت است که میتواند نقاط شکست (breakpoints) واکنشگرا را تغییر دهد یا رفتار lazy-loading را عوض کند.
این شکافها واقعی هستند. آنها مکانیکی هستند. تظاهر به اینکه اینها تصادفی هستند، باعث از بین رفتن آنها نمیشود.
محیطهای پیشنمایش (Preview Environments) دروغ میگویند
محیطهای پیشنمایش مشکل را تشدید میکنند. آنها برای بررسی انسانی مفید هستند، اما محیط تولید (production) نیستند. آنها اغلب به جای میزبان واقعی API، به api-staging اشاره میکنند. پرچمهای ویژگی (feature flags) برای هر آزمایش مقدار true برمیگردانند و منطق شرطی را که در محیط تولید اجرا میشود، پنهان میکنند. احراز هویت ممکن است یک مرحله را نادیده بگیرد یا یک توکن جعلی (mock token) تزریق کند. کوکیها ممکن است از سیاستهای منعطف استفاده کنند. مجموعه دادهها ممکن است بسیار کوچک باشند، مثلاً ده ردیف به جای ده هزار ردیف، که این یعنی منطق صفحهبندی (pagination)، رتبهبندی جستجو یا مجازیسازی (virtualization) هرگز آزمایش نمیشود.
اگر تست شما روی یک URL پیشنمایش پاس میشود اما در محیط تولید با شکست مواجه میشود، یا برعکس، مشکل از تست نیست؛ مشکل از محیط است.
قبل از حدس زدن، لاگ بگیرید
وقتی اولین بار با یک شکست مواجه میشوید، در برابر غریزه تغییر دادن تست و امید داشتن به نتیجه، مقاومت کنید. دست از حدس زدن بردارید. شما باید زمینه (context) را ثابت نگه دارید تا بتوانید یک اجرای موفق را با یک اجرای شکستخورده مقایسه کنید.
مظنونین بدیهی را لاگ کنید. آدرس URL صفحه را در لحظه شکست، شناسه بیلد (build ID) و commit SHA را ثبت کنید. پرچمهای ویژگی فعال را یادداشت کنید. میزبان API، نسخه دقیق مرورگر و اندازه viewport را ثبت کنید. این جزئیات، یک شکست مرموز را به یک شرایط قابل بازسازی تبدیل میکند.
فقط به اسکرینشاتها تکیه نکنید. دو صفحه میتوانند از نظر پیکسلی کاملاً یکسان به نظر برسند، در حالی که جاوااسکریپتهای کاملاً متفاوتی را اجرا میکنند. یک اسکرینشات به شما نخواهد گفت که باندل CI شامل یک polyfill اضافی بوده یا باندل محلی یک تکه (chunk) را نادیده گرفته چون از قبل در کش مرورگر شما بوده است.
همچنین به یاد داشته باشید که باز کردن DevTools زمانبندی را تغییر میدهد. DevTools میتواند جمعآوری زباله (garbage collection) را به تعویق بیندازد، اولویتبندی شبکه را تغییر دهد و برخی بهینهسازیهای رندرینگ را غیرفعال کند. تستی که در حالی که شما در حال بررسی DOM هستید پاس میشود، ممکن است به محض اینکه پنل را میبندید و آن را به صورت headless اجرا میکنید، شکست بخورد. دیباگر یک ابزار مفید است، اما یک ناظر بیطرف نیست.
صحنه جرم را بازسازی کنید
اگر میخواهید شکست را به دقت بازتولید کنید، نمیتوانید صرفاً سرور توسعه محلی خود را اجرا کنید و امیدوار باشید که همه چیز خوب پیش برود. شما باید شرایط دقیق CI را بازسازی کنید.
دقیقاً همان آرتیفکتی را بسازید که CI تولید کرده است. اگر لازم است آن را دانلود کنید. آن آرتیفکت را با یک سرور فایل استاتیک ساده سرو کنید، نه با Vite یا Webpack dev middleware. از همان متغیرهای محیطی (environment variables) که CI تزریق کرده است استفاده کنید. نسخه مرورگر را دقیقاً مطابقت دهید. آن را در همان حالت (mode)، چه headed و چه headless، اجرا کنید؛ زیرا رویدادهای focus، media queries و سیاستهای autoplay همچنان به شکلی ظریف بین این دو تفاوت دارند. اگر CI شما از یک کانتینر Docker استفاده میکند، همان ایمیج را به صورت محلی اجرا کنید. پروفایل شخصی مرورگر خود را کاملاً حذف کنید.
وقتی بازسازی محلی (local reproduction) بالاخره با شکست مواجه شد، شما یک جلسه دیباگ واقعی خواهید داشت. تا آن زمان، فقط در تعقیب سایهها هستید.
خوابیدن را متوقف کنید، منتظر ماندن را شروع کنید
رایجترین پاسخ به یک تست مرورگر ناپایدار (flaky)، اضافه کردن تأخیر است. پنج ثانیه صبر کنید. ده ثانیه صبر کنید. این یک راه حل نیست؛ این یک تسلیم شدن است. تأخیرهای خودسرانه باعث کند شدن مجموعه تستهای شما میشوند، اعتماد کاذب ایجاد میکنند و همچنان تحت فشار (load) و هنگام نوسانات شبکه با شکست مواجه میشوند.
در عوض، منتظر شواهدی از وضعیت (state) باشید. اگر قرار است پس از ارسال یک فرم، یک اعلان (notification) ظاهر شود، منتظر گذشت زمان نمانید. منتظر بمانید تا یک شناسه (ID) مشخص برای آن اعلان در DOM وجود داشته باشد. اگر یک شمارنده باید افزایش یابد، منتظر تغییر مقادیر متن باشید. اگر یک وضعیت بارگذاری (loading state) مانع از تعامل میشود، منتظر ناپدید شدن نشانگر بارگذاری بمانید. اگر با WebSocket یا server-sent events کار میکنید، منتظر بمانید تا جریان شبکه یک رویداد خاص تولید کند.
انتظارهای صریح (Explicit waits)، تست شما را از یک بازی حدس و گمان به یک قرارداد تبدیل میکنند. تست میگوید: «زمانی ادامه میدهم که اپلیکیشن تأیید کند آماده است.» این بسیار قویتر از این است که بگویید: «زمانی ادامه میدهم که به اندازه کافی ثانیه سپری شده باشد.»
هایدرِیشن (Hydration) و دکمهی ناپدید شونده
در اپلیکیشنهای مدرن React، فرآیند hydration باعث بروز نوع خاصی از شکستها میشود که سرورهای توسعه محلی اغلب آنها را پنهان میکنند. سرور HTML را میفرستد. React در مرورگر بالا میآید و listenerهای رویداد را متصل میکند. در آن بازه زمانی، ممکن است تست شما روی یک دکمه کلیک کند. سپس React در طول فرآیند hydration، آن گره DOM را جایگزین یا بازسازی میکند. ارجاعی (handle) که فریمورک تست شما نگه داشته بود، اکنون به یک گره جدا شده (detached node) اشاره میکند و شما با خطایی مبنی بر تعامل با یک عنصر حذف شده مواجه میشوید.
راه حل این نیست که یک selector پیچیدهتر بنویسید که عمیقتر در درخت کامپوننتها جستجو کند. راه حل این است که به دنبال سیگنالهای آمادگی باشید. منتظر بمانید تا یک عنصر ریشه (root element) ویژگی hydrated یا یک ویژگی دادهای (data property) مشخص را به دست آورد. منتظر ناپدید شدن یک skeleton loader بمانید. منتظر فعال شدن یک handler رویداد در سمت کلاینت باشید. اجازه دهید اپلیکیشن قبل از اینکه کلیکها را ارسال کنید، اعلام کند که پایدار شده است.
مقصران پنهان: وابستگیها و اسکریپتهای شخص ثالث
گاهی اوقات محیط تغییر میکند، در حالی که کد اپلیکیشن شما تغییری نکرده است. یک بهروزرسانی انتقالی (transitive update) در یک کتابخانه کاربردی کوچک، که سه سطح در node_modules شما قرار دارد، میتواند رفتار مرورگر را تغییر دهد. این تغییر ممکن است نحوه resolve شدن promiseها، نحوه تزریق استایلها یا نحوه رهگیری درخواستها توسط mockها را تغییر دهد. وقتی تستها پس از یک بهروزرسانی روتینِ وابستگیها شروع به شکست خوردن میکنند، نسخه package manager و checksum فایل lockfile خود را ثبت کنید. شما باید بدانید که آیا در حال بررسی همان درختی هستید که هفته گذشته مشاهده میکردید یا خیر.
اسکریپتهای شخص ثالث (third-party) خرابکاران رایج دیگری هستند. ردیابهای آنالیتیکس، SDKهای پرداخت و ویجتهای چت به صورت ناهمگام (asynchronously) بارگذاری میشوند. آنها در لحظاتی که تست شما انتظار ندارد، iframeها را تزریق میکنند، چیدمان (layout) را جابجا میکنند یا focus را میدزدند. در CI، این اسکریپتها ممکن است با سرعت کمتری بارگذاری شوند، یا به دلیل محدودیتهای شبکه اصلاً بارگذاری نشوند و باعث شوند اپلیکیشن شما مسیر مدیریت خطای متفاوتی را دنبال کند. ثبت کنید که کدام منابع شخص ثالث بارگذاری شدهاند و وضعیت HTTP آنها چیست. اگر یک iframe پرداخت در CI سه ثانیه طول میکشد تا mounted شود اما در اتصال محلی سریع شما بلافاصله بارگذاری میشود، خطای "element not clickable" شما ناگهان دلیل مشخصی پیدا میکند.
و "element not clickable" هرگز یک تشخیص (diagnosis) نیست؛ بلکه یک علامت (symptom) است. با علت برخورد کنید.
یک کیت شواهد بسازید
هر شکست در CI باید قابل اقدام (actionable) باشد. داشتن یک stack trace به تنهایی کافی نیست. شما به یک کیت شواهد (evidence kit) نیاز دارید که به مهندس دیگری، یا خودتان در ماه آینده، اجازه دهد آنچه اتفاق افتاده را بازسازی کنید.
اسکرینشاتها و ضبط ویدیوهای مربوط به اجرای ناموفق را نگه دارید. خروجی کامل کنسول مرورگر را ثبت کنید، نه فقط خطاها بلکه هشدارها را نیز. شکستهای شبکه، از جمله خطاهای 404، رد شدنهای CORS و قطع شدن اتصالات را لاگ کنید. شناسههای build و feature flagهایی که فعال بودند را حفظ کنید. در دقیقاً همان لحظهای که assertion شکست خورد، یک snapshot از DOM بگیرید. یک snapshot به شما اجازه میدهد ساختار HTML را پس از وقوع حادثه بررسی کنید، به جای اینکه...
