نوشتن unit test برای پروژه هامون(چه شخصی و چه بیزنسی) یکی از مهم ترین مواردیه که باید رعایتش کنیم . اما نکته ای که وجود داره اینکه گاها به تست ها و کیفیت اون ها زیاد توجه نمیشه و با نوشتن تست های بد بازم کار رو برای خودمون و یا بقیه سخت می کنیم. تغییر تو پروژه یکی از مواردیه که همیشه وجود داره(از رفع باگ گرفته تا اضافه کردن فیچر جدید). از اون طرف تست ها هم با توجه به شرایط جدید نیاز به تغییر پیدا میکنن(لزوما نه همیشه). حالا اگر تست های ما به شکلی نوشته شده باشند که ایجاد تغییر تو اونا سخت باشه چه اتفاقی میوفته؟ بعد از یکم وقت گذاشتن روی موضوع و نا امید شدن از تغییرش، یه نیگا به این ور و یه نیگا به اون ور و اون تست کامنت یا پاک میشه!!!!
طبیعتا راه حلش هم اینکه تست های خوب و با کیفیت که بعدا دستمون رو تو پوست گردو نزاره، بنویسیم! در باب اهمیت تست نویسی خوب هم بگم که تو کتاب clean code هم گفته شده که اهمیت کد تست از کد خود محصول(production) کمتر نیست و تمام مواردی که باید برای نوشتن یک کد تمیز لازمه رو باید تو تست نویسی هم رعایت کنیم.
تو این مطلب هم سعی شده چند نکته درباره تست نویسی خوب گفته بشه.
نکته: اینکه چرا ما باید برای پروژه هامون تست بنویسیم و مورادی از این قبلی هدف این مطلب نیست. خوشبختانه پست های خیلی خوبی تو این زمینه نوشته شده که توصیه می کنم اگر با این موارد آشنا نیستین حتما درموردش سرچ و مطالعه کنین.
حرف از کتاب clean code شد بیاین با نکات همین کتاب شروع کنیم.
رابرت مارتین تو کتاب خودش از ۵ ویژگی برای تست خوب یاد میکنه که به اختصار اسمش رو گذاشته: F.I.R.S.T که در ادامه به ترتیب درموردشون حرف میزنیم.
توضیح: هرچی تست هامون مدت زمان اجرای طولانی تر داشته باشند در نتیجه رغبت کمتری رو برای اجرای اون ها خواهیم داشت. بزارین این طوری توضیح بدم که فرض کنین داریم یه سری تغییرات رو تو پروژه انجام میدیم اصولا این طوریه که هر بخشی که تغییر میدیم باید تست اون بخش رو اجرا کنیم تا مشخص بشه که چیزی خراب شده یا نه؟ اما چون اجرای تست ها طول میکشه با خودمون می گیم که به جای اینکه تست ها رو ران کنم و منتظر بمونم می تونم تو این زمان تغییرات بیشتری رو اعمال کنم و آخر کار یک بار تست ها رو ران میکنم ببینم مشکلی هست یا نه! خب مشکل اینجاست که تو برنامه نویسی جزئیات خیلی تاثیرگذارن، به عنوان مثال تو دستور if شرط > با >= خیلی فرق میکنه، و موقع کد زدن هم اشتباهات کوچیک زیاد داریم. پس از اونجایی این جزئیات پس از تغییرات کوچیک بررسی نمیشن و بررسی اون ها رو به آخر کار محول می کنیم، این رفتار معمولا باعث میشه تست های بیشتری fail بشوند و از اون طرف چون یک سیستم از ماژول های مختلفی تشکیل شده، پس یک زنجیره ای از خطا ایجاد میشه. طبیعتا پیدا کردن و رفع باگ های ایجاد شده هم طاقت فرساتر و از لحاظ زمانی مدت بیشتری رو از ما خواهد گرفت. پس تست هامون باید تا جایی که می تونن سریع باشن تا ما رغبت بیشتری رو برای اجرای اون ها داشته باشیم.
مورد دوم Isolated: تست ها باید ایزوله و مستقل از همدیگه باشند.
توضیح: تست باید کاملا مستقل از همدیگه کار کنند. تمام داده هایی که نیاز دارند رو بدون وابستگی به تست های دیگه مهیا کنند و ترتیب اجرای تست ها هم مهم نباشد. همچنین تک تک تست ها رو باید بتونیم به تنهایی اجرا کنیم. اگر این کار ممکن نیست پس تست رو اشتباه نوشتیم. فرض کنیم که ما چهارتا تست داریم(فقط ۴ تا!!!) و برای اینکه تست سوم درست کار کنه باید قبل از اجرای اون تست دوم اجرا شده باشه چون تست دوم یک متغیر محیطی رو تنظیم میکنه(فرض هم کنیم که تست با ترتیب اجرا میشن). چه اتفاقی میوفته اگر کد پروژه یکم تغییر کنه و دیگه به اون بخش تنظیم متغیر محیطی تو تست نیازی نداشته باشیم؟
توضیح: تست ها نباید وابستگی به محیط و زمان اجرا داشته باشند. تست باید همیشه در هر محیط و زمانی از شبانه روز یک خروجی مشخص داشته باشد و این طوری نباشه که وقتی تو شب اجرا میشه یه خروجی بگیره و تو روز یه خروجی دیگه(درباره این موضوع تو ادامه مثال با کد هم میاریم).
توضیح: تست نباید این طوری باشه که برای بررسی fail شدن و نشدن اون بریم یک مورد دیگه بررسی کنیم مثلا یک فایل لاگ رو نگاه کنیم.
توضیح: نکته اول اینکه این مورد یکم سلیقه ای و به سیاست های خودتون و یا تیم هم وابستگی دارد. به طور مثال تیم هایی هستند که کد بدون تست رو اجازه push کردن نمیدن ولی تست هاشون لزوما قبلا از کد محصول نمی نویسند. در واقع اگر قرار باشه همیشه حتما برای کدهامون تست بنویسیم(تاکید میکنم همیشه) در این صورت میشه گفت نوشتن کد تست قبل از خود کد محصول بستگی به خودتون یا تیم تون دارد.
نکته برای این مورد به جای Timely گاها از Thorough رو هم استفاده میشه که در این صورت باید گفت:
تست باید تمام حالت ممکن رو پوشش بده. همه انواع داده های بزرگ، کوچک، معتبر، نامعتبر، شرایط
corner(در ادامه بیشتر توضیح میدیم)، انواع کاربرهای مختلف موجود در سیستم و... رو شامل بشود.
توضیح: تست ها باید نام های مشخصی داشته باشند تا با نگاه کردن به اسم اون کامل مشخص باشه که برای چه موردی نوشته شده. برای یک نام گذاری خوب می تونیم از این روش استفاده کنیم:
برای اسم هر تست سه بخش را داشته باشیم:
به عنوان مثال اسم test_single هیچ معنایی نداره ولی Add_SingleNumber_ReturnsSameNumber به خوبی مشخص برای چی نوشته شده است.
توصیح: بهش 3A هم گفته میشه که یعنی: Arrange, Act, Assert.
این کار باعث میشه خوانایی کد بیشتر باشه.
توضیح: تست های کوچک چندتا مزیت دارند:
توضیح: نام گذاری متغیر های تست به اندازه نام گذاری متغیر های کد محصول اهمیت دارد. این باعث خوانایی کد شده و به فهمیدن هدف تست نیز کمک می کند. در نتیجه در اینده اگر خودتون یا برنامه نویس دیگه ای به تست مراجعه کرد به راحتی اون رو بفهمه و تغییرش بده.
توضیح: استفاده از دستورات if, while, for, switch در تست مشکلاتی را به همراه دارد که عبارت اند از:
توضیح: ما معمولا به همه متغیرهایی که توی setup تعریف شدن نیازی نداریم. و از اونجایی که setup, teardown قبل و بعد از هر تست فراخوانی می شود پس در تست ها متغیرهایی خواهیم داشت که به اون ها نیازی به آن ها نداریم. از اون طرف فراخوانی هر باره این دو تابع مخصوصا اگر کارای زیادی رو انجام میدن مدت زمان اجرا رو طولانی تر میکنه. نکته مهم دیگه کمتر شدن خوانایی کد تست هستش. چون این دو تابع به صورت صریح توی کد فراخوانی نشدن و به طور ضمنی فراخوانی می شوند در نتیجه موقع خوندن کد برنامه نویس باید حواسش دائما به این موضوع و مواردی که تو این دو تابع وجود دارند باشه. استفاده از توابع helper این موارد را به خوبی کنترل میکنه(البته این به معنی نیست که دو تابع کلا استفاده نکنیم بلکه می تونن کمک کننده باشند).
توضیح: بزارین با یک تکه کد این مورد را ببینیم:
def get_time_of_day(): current = datetime.now() if current.hour > 12: return "afternoon" else: return "morning"
به نظرتون اگر بخوایم برای این تابع تست بنویسم به چه مشکلی میخوریم؟ یادتونه تو ویژگی های F.I.R.S.T گفتیم که تست باید Repeatable باشه و در همه شرایط و زمان ها یک نتیجه رو بگیریم. خب تو این مورد اگر ما تست رو یک بار صبح و یک بار عصر اجرا کنیم نتیجه مختلفی رو میگیرم! برای حل این موضوع باید از if استفاده کنیم از اون طرف هم گفته بودیم که داشتن if تو تست خوب نیست، پس چیکار بکنیم؟ در واقع مشکل از تست نیست و از خود تابع هستش که درست پیاده سازی نشده. تابع باید به این شکل اصلاح بشه:
def get_time_of_day(time_of_day): if time_of_day.hour > 12: return "afternoon" else: return "morning"
الان اگر بخوایم این تابع رو تست کنیم خیلی راحت می تونیم زمان رو خودمون تنظیم و به تابع پاس بدیم و خروجی مورد انتظار رو assert کنیم.
توضیح: اگر در محصول ما باگی پیدا شد یعنی برای اون حالت تست نداریم. پس برای جلوگیری از این باگ در آینده باید برای اون تست بنویسیم و به مجموعه تست هامون اضافه ش کنیم.
توضیح: خود پیدا کردن این حالت ها هم جالبه حتی(برای درک این موضوع لینک ها رو بخونین)!
نکته آخر هم اینکه بعد از pull، قبل از push و بعد از هر تغییری تست ها باید اجرا بشوند.
مواردی دیگه ای هم میشه مطرح کرد که دیگه شاید باعث بشه مطلب خیلی طولانی بشه به خاطر همین اگر دوست داشتین می تونین با سرچ کردن مطالعه شون کنین.
همین، شاد و خندون باشین و امیدوارم که تست هاتون کمتر fail بشن. :)
منابع: