(ویرایش: یک سیستم نمونه در نوشتار دیگری ارائه شده که برخی مفاهیم نوشتار زیر را ملموستر میکند.)
برای داشتن یک سیستم نرمافزاری قابل اعتماد، سیستم شما لایههای فزایندهای از تست خواهد داشت و همه میدونیم چرا. ولی در رویِ کمتر بحث شدهی سکه، شاهد تیمهای گوناگونی بودهام که زیر هزینههای فزایندهی این تستها کمر خم میکنن/میکنیم علیرغم تلاشهای فراوان (حتی تشکیل تیمهای مختص) برای خروج از این وضعیت. از پیش بگم که راه حل جادویی عمومی وجود نداره و کاملا به طبیعت سیستم و نیازمندیهای کسب و کار بستگی داره. هدف این گفتار به بحث گذاشتن تلهایست که سیستم به سمتش میره. همچنین نیاز به گفتن نیست که این مشکلات به هیچ وجه به معنی «تستهای کمتری داشته باشیم» نیست.
مرور خیلی خیلی کوتاهی بکنیم بر «چه تستهایی و کِی» تا همه برسیم سر یک صفحه. اگر سیستمی دارید با continuous integration یا CI خوب و مکفی، میتونید از این بخش رد بشید.
سیستم CI (مثلا Jenkins) قراره با منبع کُد (مثلا Git) و سیستم بازبینی کد (مثلا Gerrit) کار کنه، و اولا سرِ منبع کد (HEAD) رو به طور پیوسته و دوما هر changelist/patch توسعهدهندگان رو پیش از اجازهی commit، در برابر همهی تستهای موجود بسنجه. البته شاید تصور بشه که با وجود دومی، اولی زیادیه ولی چنین نیست، و نیز شاید به ازای «هر» changelist شدنی نباشه که دیگه جزییاته. خلاصه سرِ منبعِ کد همیشه تمیز است و آمادهی بروندهی (release). از بروندهی و قناریگری اگر عمری بود بعدا خواهم نوشت.
حالا چه تستهایی؟ برای یک سیستم معمولی شامل چند سرویس که با هم تعامل میکنن---اون مستطیلهایی که به عنوان معماری سیستم روی تخته میکشید و در قالب یک درخت یا DAG از فراخوانیها با پیکان به هم وصل میکنید---در نقطهی بلوغ احتمالا صدها یا هزارها تست واحد (unit test) برای کُدها، چندین و چند جور تست یکپارچهسازی (integration test)، تستهای بار و چکشخواری سرورها (load/stress/resilience test)، تستهای الف/ب (A/B) و پیشگیری از پسرفت (regression)، و شاید تستهای دیگر بسته به ماهیت سرویسها خواهیم داشت. سپس احتمالا یک یا چند مرحله قناری برای فرآیند بروندهی هر یک از سرویسها.
نام تستها مهم نیست، شاید شما به نامهای دیگری بشناسید. تست واحد نیازی به توضیح نداره، فقط یک خطای رایج به ویژه توسط توسعهدهندههای تازهکار که باید هنگام بازبینی کُدشون حواسمون بهش باشه اینه که برخی تستها نوشته نمیشه چون زیادی بدیهیاند، ولی فراموش میشه که تست واحد فقط برای امروز نیست بلکه حِفاظی برای سوتیهای احتمالی در تغییرات آینده (که کد شاید نابدیهی میشه) هم هست. سپس جزءهایی که در انزوا (isolation) تست شدن رو کنار هم میذاریم و مجموعهشون رو تست میکنیم: از مجموعهی زیرمولفهها به یک مولفه، به یک سرویس، به مجموع سیستم، و خلاصه هر چیزی این میان میشه تست یکپارچهسازی، مثلا ورودی الف رو میدیم که بره در مجموعه بچرخه و در پایان انتظار خروجی ب رو داریم. تستهای بار برای سنجیدن کارایی سرویس و واکنش به فشار است و تست الف/ب برای اطمینان از چیزی که اصطلاحا بهش میگن «گذار سبز به سبز»، مثلا بین HEAD کنونی و changelist جدید یا بین نسخهی بروندهیشدهی کنونی و کاندید بعدی نباید تفاوت غیرمنتظرهای باشه.
تا اینجا زندگی به نظر سادهست و انواع تستهای نام برده و نام نبرده هم در منابع فراوان تشریح شدهاند.
در پرانتز: همهی آنچه گفته شد،در حالت ایدهآل، بدون مداخلهی انسانیست، یعنی رسیدن به کیمیای «push on green» - در این باره این مقالهی خواندنی از گوگل رو ببینید. ولی بسیاری از تیمهای خود گوگل به این کیمیا نرسیدن. البته باز هم مداخلهی انسانیِ باقی مانده، در خط دوم دفاعی است مثلا مرور نتیجههایی که اتوماتیک سبز نشدن. اگر در سیستم شما، مداخلهی انسانی در خط اول دفاعیست، مثلا هر بار دستی اجرا کردن تستها و دستی چک کردن نتایج، جا داره یک بازنگری با دید بلند مدت بکنید - احتمالا کارهای بهتری بشه با اون نیروی انسانی کرد.
مثال برای بقیهی متن: فرض کنید یک فروشگاه اینترنتی داریم با قابلیتهای سادهای مثل فروش کالاهای خودمون و نیز فروشندههای بیرونی، امکان جستجو و خرید برای مشتری، امکانی برای فروشندههای بیرونی برای ترویج کالا در نتایج جستجو و پیشنهاددهی به کاربر و غیره.
سیستم ما که همهی تستهای گفته شده رو پاس کرده بود، ناگهان در دنیای واقعی سوتی میده علیرغم این که پوشش تست (test coverage) خوب داشت. مثلا برای نصف شهر خطای ارسال کالا میدیم یا کالاهای فروشندههای بیرونی رو نمیفروشیم. چرا؟ چون مثلا فلان field فلان protocol buffer که یک تکه از نسخهی جدیدمون روش حساب میکنه مدتیست که دیگه در ورودی set نمیشه چون apple یک update جدید داده، یا چون فروشندههای بیرونی ابزار اتوماسیون راه انداختن برای تعامل با سیستم ما و رفتارشون عوض شده، یا هر اتفاق ارگانیک و غیرارگانیک دیگری. خلاصه: تست «ورودی الف رو بده و خروجی ب رو ببین»، در دنیای واقعی که از دیروز به امروز و از اینجا به اونجا زمین بازی عوض میشه، واقعگرایانه نیست.
طبیعتا این شکاف رو با تستهای واقعیتر پر میکنیم: درخواستهای واقعی و تازه از محیط عملیاتی (به جای درخواستهای مصنوعی یا حتی واقعی ولی ایستا)، دادهها/datasetهای زنده و به-روز، مدلهای یادگیری شدهی زنده، حتی اجازه دادن به سرورِ زیرِ تست برای صحبت با backendهای واقعی به جای ماکت.
احتمالا حدس زدید که درِ چه لونه زنبوری رو باز کردیم: عدم تَعَیّن (non-determinism). یعنی:
مشابه یعنی چی؟ یعنی باتلاق. یعنی اگر بگید «الف مشابه ب = الف تا ۱۰٪ی ب» (تازه به این فرضِ غلط که همهی fieldها اعداد حقیقیاند و نه مثلا رشته)، به دلیل عدم تعیّن، گاه و بیگاه روی چندتا از فیلدهای مقایسه شده خطای الکی میگیرید. یعنی اگر الف رو با خود الف هم مقایسه کنید اختلاف/خطا میگیرید. هنگام تست کردن یک سرویس با ورودی و دادهها و مدلهای زنده و backendهای واقعی، دیگه مساله فقط ثابت نگه داشتن چندتا random seed محلی برای بازتولیدپذیری (reproducibility) نیست. سپس برای هر یک field از دهها و صدها field مقایسه شده، شروع میکنید به شُل کردن آستانهها (thresholds) برای این field و اون field، دهها و صدها تا. فرسایشی بودنِ این امر برای تیم به کنار، خودِ شل کردن آستانهها یعنی دانهدرشتتر کردن توری که برای شکار تغییرات پهن شده بوده، یعنی چیزهای بیشتری که قرار بوده شکار و جلوگیری بشن دیگه نمیشن. پس به همین راحتی هم آستانه رو برای این field و اون field شل نمیکنیم بلکه برای هر یک از اون دهها و صدها مورد شل کردن، وقت و انرژی صرف بررسی میکنیم.
نتیجه؟ وقتی کُدتون رو نوشتید و changelist شما رفت توی سیستم CI، در تستها رد میشه - اگر فقط ۴تا تست داشته باشید که فقط ۲۰٪ مواقع متزلزل باشن، changelist شما به احتمال ۵۹٪ حداقل یکیش رو رد میشه حتی اگر فقط مثلا rename کردن یک متغیر باشه. این مشکل از قضا در تیمهای ما زیاده چون سیستم و تستها در طی سالیان دیگه زیادی بزرگ پیچیده شدن و بازگشت و بازطراحی سخته. نتیجه؟
اگر در هر یک از مراحل بالا به ذهنتون رسید که «سخت نگیریم»---مثلا در مثالِ سیستم پیشنهاددهی فکر کردید تنها پیشنهاد نهایی رو چک کنیم یا در مثالِ شُل کردن آستانهها به ازای هر field فکر کردید حالا به ازای هر field نباشه و یک آستانهی کلّی رو برای مجموعهی پاسخ شل کنیم---باید بگم فرق یک محصول با کیفیت جهانی و یک محصول که down بودنش برای مشتری عادی شده در همین وسواس روی کوتاه نیامدن سرِ کیفیته - البته قصدم پُز دادن و نگاه بالا به پایینِ معلّمی نیست بلکه نشان دادن جدیّت کاره.
چنان که گفته شد، راه حل جادویی و عمومی وجود نداره و به طبیعت سیستم بستگی داره.