این سری پستها در مورد تجربه یک گشت و گذار دنبال جواب این سوال هست که من چجوری مطمئن دشم تستهای درستی مینویسم. به من وظیفهای محول شده بود که تعدادی تست E2E برای شرکت بنویسم و این سوالها در ادامه اون تجربه برام بوجود اومدن.
همونطور که در قسمت اول این پست گفتم ما تست واحد داشتیم ولی تستهایی که بتونن کارکرد سیستمرو در لایههای دیگه ارزیابی کنن وجود نداشت. از جمله تستهای I&T که میتونست کمک بزرگی برای تضمین کیفیت نرمافزار باشه ولی ما سراغش نرفته بودیم. تستهایی که من قرار بود روشون کار کنم، تستهای E2E بودن و در نتیجه من باید تستهایی با متد blackbox testing مینوشتم که نرمافزارها رو توی محیط تستی نصب کنم و به همون شکلی که یک مشتری با این نرمافزارها کار میکنه، من هم کار کنم. یعنی نمیتونم به داخل سیستم دست بزنم و فقط باید رفتار بیرونی سیستم رو بررسی کنم.
قطعا در متدهای whitebox testing مثل تست واحد، انعطاف خیلی بیشتری در تست کارکردها وجود داره. این تستها یک بخش کوچک از سیستم رو تست میکنن و معمولا اون بخش رو تا حد ممکن از بقیه سیستم ایزوله میکنن که باعث میشه کنترل بیشتری روی جزيیات رفتار سیستم داشته باشن.
خود تستهای واحد همیشه جزو مهمترین تستهای نرمافزار در هر پروژهای هستن. این تستها نسبتا راحت نوشته میشن، کد رو در سطحی از جزئیات تست میکنن که اغلب برای باقی تستها ناممکن هست و همینطور خیلی سریع اجرا میشن. همون چیزی که test automation pyramid میگه:
اگر شما تست واحد مینویسید، قطعا ابزارهای بیشتری برای ارزیابی کیفیت تستهای نوشته شده شما وجود داره و راحتتر میتونید به سوالاتی مثل سوالات بالا جواب بدید. مثلا یکی از ابزارهای محبوب در این زمینه code coverage هست. ایده پشت این معیار اینه که تستهای شما باید باعث بشن خط به خط کدهای نرم افزار شما یا درصد خیلی بالایی از اونها حین تستها اجرا بشن (مثل کدهای داخل تمام توابع، تمام ifها، تمام switch caseها). میزان پوشش تستها هرچقدر به ۱۰۰٪ نزدیکتر باشه، میشه اطمینان بیشتری داشت که ما داریم همه جای کد رو تست میکنیم و کم شدن درصد پوشش به مرور زمان، نشاندهنده این هست که سرعت نوشتن تستها از سرعت توسعه نرمافزار کمتره و خیلی چیزهای جدید نوشته شدن و به کد اضافه شدن که هیچ تستی براشون نوشته نشده.
ولی یکی از بامزهترین ابزارهایی که من باهاش برخورد کردم، Mutation testing هست که یک قدم هم از code coverage جلوتر رفته. ایده تست جهش اینه که اگر ادعا میشه تستهایی که code coverage صددرصد دارن و همه چی رو تست میکنن، پس اگر وقتی همه سبز هستن، قسمتی از کد نرمافزار رو تغییر بدیم، مثل تغییر مقدار یک متغیر داخل کد، یا تغییر یک شرط (if(x < n) به if(x > n)) انتظار میره که حداقل یکی از تستها قرمز بشه و این جهش رو reject کنه. دقت کردید چی شد؟ یعنی همونطور که تستها کیفیت نرمافزار رو ارزیابی میکنن، خود نرمافزار هم برای ارزیابی کیفیت تستها به کار میره!
البته باید گفت که نه این معیار و نه حتی معیار code coverage همیشه قابل استفاده نیستن و معمولا به کار نرمافزارهای بزرگ نمیان. ولی به نظرم اومد چون باحال هستن ارزش اشاره کردن رو دارن :) (البته جلوتر باز هم به ایده کلی پشت هر دو اینا بر میگردیم.)
خب حالا برگردیم به تستهای E2E. بعد از خوندن در مورد تستهای مختلف، من به چند تا نتیجه رسیدم. اول اینکه ما معیاری به خوشدستی چیزهایی مثل دو تا معیار بالا برای تستهای E2E نداریم. ولی شاید بشه با تغییر صورت مساله راهحلهایی براش طراحی کرد.
اول اینکه شاید نشه معیاری مثل code coverage رو برای نرمافزارهای شرکت ما به کار برد، ولی شاید بشه معیار coverage رو تغییر داد. تنها چیزی که باید در تستها cover بشه فقط اجرای خطهای سورسکد نیست.
اول بزارید این سوال رو بپرسیم که اصلا چرا test coverage در هر پروژهای مهم هست؟ بزارید اول به این اشاره کنم که چیزی وجود داره به نام regression testing. این تستها تضمین میکنن که تغییر جدید در سورسکد نرمافزار هیچ تغییری در رفتار نرمافزار، در هیچ سطحی از کارکردها (از رفتار کلی سیستم تا رفتار یک متد یا api) ایجاد نمیکنه (به زبان خودمونی، در ازای هر رفع باگ توسط یک برنامهنویس خلاق و تجربهگرا، ۳ تا باگ جدید در سیستم تولید نمیشن!). تستهای رگرسیون میتونن شامل همه تستهایی باشن که تا اینجا در موردشون صحبت کردیم، فقط تنها شرطشون اینه که باید بعد از هر تغییر در کدهای نرمافزار اجرا بشن تا فوری تشخیص بدیم کدوم تغییر در سورس نرمافزار باعث تغییر در رفتار سیستم شده (همون کاری که در CI/CD هم انجام میشه). در نتیجه میتونیم هر خطایی در بیلد شدن نرمافزار رو فوری تشخیص بدیم و بدونیم برای اون خطا باید سراغ کدون تغییرات در سورس برنامه بریم. فایده بالا نگه داشتن coverage تو اینجا چیه؟ هر چقدر پوشش بالاتر باشه، باعث میشه مطمئن باشیم که تعداد این مدل خطاها که از دستمون در میرن و نمیتونیم در لحظه اول شناساییشون کنیم کمتر هستن.
خب، من گفتم coverage میتونه برای چیزهای دیگهای هم مطرح بشه. مثل چی؟ مثل درصد پوشش از لیست رفتارهای مورد انتظار در سیستم. برای مثال از یک اپلیکیشن فروشگاهی انتظار میره فیچر نمایش لیست خرید رو داشته باشه. فیچری مثل نشون دادن لیست کالای انتخاب شده توسط کاربر در سبد خرید به همراه قیمت کلی اونها. برای نمونه این سناریو رو در نظر بگیرید:
سه کالای مداد به قیمت ۱ دلار و خودکار به قیمت ۲ دلار و دفتر به قیمت ۳ دلار در فروشگاه برای خرید موجود هستن. اکانت کاربر تست هیچ چیزی در سبد خرید خودش نداره. کاربر یک مداد و دو دفتر به لیست خرید اضافه میکنه. وقتی کاربر به صفحه سبد خرید میره، یک مداد به قیمت ۱ دلار و دو دفتر هر کدوم به قیمت ۳ دلار در سبد هستن.
یک فیچر نمایش سبد خرید میتونه چندین سناریو برای تست داشته باشه. مثلا هزینه کل سبد، محاسبه و نمایش تخفیفها و امکان حذف از سبد هم چیزیه که یک تست جداگانه باید براشون طراحی بشه.
ما میتونیم برای سیستم خودمون یک لیست درست کنیم از فیچرهایی که ادعا میکنیم سیستممون قراره ساپورت کنه (یا لیستی از فیچرها که مشتری انتظار داره سیستم ما اونها رو ساپورت کنه). هر کدوم از فیچرهای این لیست هم چند سناریو داشته باشن که رفتار مورد انتظار فیچر در حالتهای مختلف رو تشریح میکنه. و ما میتونیم معیار coverage رو اینطوری تعریف کنیم که تستهای ما چند درصد از فیچرهای این لیست و سناریوهای مختلف هر فیچر رو پوشش میدن؟ اگر ما بتونیم درصد بالایی از این لیست رو پوشش بدیم، در واقع تونستیم یک معیار coverage با کارایی توجیهپذیر بسازیم.
ولی این تمام چیزی که میخوام بگم نیست. در دنیای تست نرمافزار چیزی وجود داره به نام Behavior Driven Development یا BDD. یکی از مهمترین ویژگیهای BDD این هست که سناریوهای تست رو به زبانی غیر فنی ولی قابل فهم برای کامپیوتر بیان میکنه. این تستها به زبان Gherkin نوشته میشن که مثلا برای فیچر و سناریو بالا به این شکل هست:
Feature: Application has cart to store and show order info Scenario: Add some products and check them in cart Given Website has given products | prod_name | price | | pencil | 1 | | pen | 2 | | notebook | 3 | And test_user has no product in cart When user adds 1 pencil to cart And user adds 2 notebook to cart And user opens cart Then cart has 1 pen with price 1 And cart has 2 notebooks with price 2 Scenario: Remove some of selected products from cart ....
این زبان با سینتکس سادهاش چجوری کار میکنه؟ اول توی کد چند تا عبارت اصلی داریم:
و بعد توسعه دهنده باید هر کدوم از این گامها (جملات محاوره) رو به یک تابع وصل کنه تا فریمورک تست شما بدونه با هر جمله چه کارهایی باید انجام بشه. مثل این کد در پایتون:
@when('user adds {amount} {product_name} to cart') def step_add_cart(context, amount, product_name): .....
تست BDD مزایای زیادی داره. اینکه شما میتونید یک تست رو به شکل غیرفنی بنویسید، باعث میشه آدمهای غیرفنی شرکت هم بتونن در پروسه تست سهیم باشن، و علاوه بر این، در اصافه کردن هر فیچر جدید، این زبان در عین راحتی و قابل فهم بودن برای همه، جزئیات فنی و دقیقی از اونچه توسعهدهنده نیاز داره رو هم در اختیارش میزاره(البته کاملا سطح بالا هست و توی جزئيات نمیره). این تستها برای من یک مزیت خیلی مهم دیگه هم داشتن، من مطمئن بودم که هیچ کسی هیچوقت سراغ تستهای فعلی من نمیره که ببینه من واقعا تو اون تست چی نوشتم، مگر موقعی که سیستم دچار مشکل بشه و همه برن سراغ اینکه چرا تستها مشکل رو تشخیص نمیدادن. در حالی که نوشتن تستها به زبان Gherkin در واقع میتونه تستها رو همزمان داکیومنت کنه و اینطوری انتظار میره اون تست مشکلدار راحتتر توسط یکی از آدمهای فنی یا غیر فنی شناسایی بشن.
تا اینجا، بعد از معرفی چند نوع تست، معرفی خیلی مختصری از تستهای BDD داشتم. در بخش سوم و پایانی، بعد از پاسخ به سوالات قسمت اول، جمع بندی درباره چیزهایی که گفتم رو خواهم داشت.