توسعه تست محور (Test Driven Development)، یک توصیه اصلی دارد: ابتدا تست نوشته شود. در بسیاری از آموزشها و کتابها در صورت نوشتن تست هم، این عمل قبل از توسعه کد اصلی انجام نمیشود. متاسفانه این روند در توسعه نرمافزار نیز به صورت یک رفتار غریزی نمود پیدا میکند. گاها موارد توسعه تست، یک ارزش از کد نوشته شده و فرد توسعه دهنده در نظر گرفته نمیشود و در نتیجه بسیار مورد بیمهری قرار میگیرد. در نوشتن تست برای یک کد مواردی مشاهده میشود که در ادامه برخی از این موارد عنوان شده است و راه حل اکثر این موارد یک توصیه است: تست قبل از کد نوشته شود.پثیهعپ
فرض کنید کدی توسعه یافته است، در جواب اینکه آیا تست برای این کد نوشته شده است یا نه، پاسخ داده میشود، آری. با بررسی عناوین تستها نیز این مساله مشخص میشود که تستها به صورت صحیح انجام شده است. اما باید به یک نکته توجه شود، اینکه اگر مشکلی در تستهای توسعه یافته موجود باشد، غیر از بررسی کد، به صورت دیگری قابل پیگیری نیست. میتوان تست دروغگو را به این صورت تعریف کرد: تستهایی که بر خلاف اسم تست، روند درستی را مورد بررسی قرار ندادهاند.
کد تست زیر را در نظر بگیرید:
این تست برای یک تابع توسعه پیدا کرده است که وظیفه محاسبه مالیات را بر عهده دارد. تابع مورد نظر، در صورتی که مقدار مجموع هزینه کمتر از یک آستانه باشد، مقدار صفر برگشت میدهد. تابع تست نیز بعد از دریافت نتیجه نهایی، مقدار مالیات خروجی را مورد بررسی قرار داده است، نوع ادعای (assertion) مورد استفاده مشکل دارد. در ادعای تست عنوان شده است که اگر مقدار بازگشتی بزرگتر یا مساوی عد مورد نظر باشد، تست صحیح است. یعنی برای هر عددی این تست صحیح خواهد بود. اگر در طول مسیر توسعه، فرد دیگری تابع هدف را تغییر دهد و اصل مقدار آستانه را دچار مشکل کند، این مورد در هنگام انجام تست قابل ردیابی نیست و در زمان اجرای نهایی مشکل مشخص خواهد شد.
در این مثال خاص، ادعا با عنوان تست همخوانی ندارد و یا باید عنوان تست تغییر کند یا ادعای تست تغییر کند. توجه شود که در تستهای دروغین متاسفانه، پوشش کد نیز افزایش پیدا میکند و این باعث میشود یافت مشکل بعد از توسعه بسیار دشوار شود.
راه حل برای اینگونه مشکلات، استفاده از روندهای pair programming یا جدی گرفتن روند بازبینی کد است.
مشخصا هر تستی نیاز به آماده سازی دارد. آمادهسازی ماکها، متغیرهای اولیه، ساخت شی از هدف تست و امثالهم. این آمادهسازی باید در حد توازن و تا جای ممکن کم باشد. هر اندازه این روندهای آمادهسازی بیشتر باشد، نگهداری و تغییر کدهای تست دشوارتر خواهد بود. با توجه به اصول TDD که برای هر توسعه کد ابتدا باید تست نوشته شود، در صورت نیاز به آمادهسازی زیاد، این روند خود به یک عامل مشکلساز تبدیل میشود. همچنین آموزش نیروهای جدید نیز در این مورد بسیار دشوارتر میشود.
اینگونه موارد باعث یک مشکل اصلی در کد توسعه یافته نیز میشود: همبستگی بین کد و تست را زیاد میکند. مشخصا تست وابستگی با کد مورد تست دارد، اما این وابستگی تا حد ممکن در نتیجه نهایی پذیرفته است. به این مفهوم که تست یک قسمت از کد، تا حد امکان باید فقط از آن قطعه کد تاثیر پذیر باشد و تغییر یک قسمت دیگر، نباید باعث شکست یک یا چند تست دیگر شود که ارتباط مستقیم با این واحد ندارند.
اینگونه از مشکلات در مواقعی پدید میآید که طراحی مورد نظر دارای ماژولاریتی ضعیت بوده و اصل جدایی مفاهیم (separation of concerns) در آن به خوبی رعایت نشده باشد. به عبارت دیگر اگر در کد توسعه یافته روند Single Responsibility (از اصولSOLID) در کد رعایت نشده باشد، چندین آمادهسازی برای تست یک واحد کد بزرگ مورد نیاز است.
راه حلی که در این مورد اغلب مورد استفاده قرار میگیرد، بهبود انتزاع در کد و تغییر در طراحی بخشهای کد به منظور بهبود جداسازی کد است. اما یک مساله مهم در این قسمت جلب توجه میکند: تغییر ساختار هزینه بالایی دارد و در بسیاری از موارد قابل انجام نیست. این مساله به این دلیل به وجود می آید که تغییر ساختار معلولی از علت دیگری است. علت اصلی به وجود آمدن مشکل آمادهسازی زیاد، عدم توسعه تست قبل از توسعه کد اصلی است (در نظر نگرفتن اصول درست TDD و استفاده غیرصحیحی از این اصل). اگر ابتدا تست یک تابع نوشته شود، در صورت زیاد شدن آمادهسازی، قبل از توسعه کد اصلی قابل پیگیری و تغییر رویکرد طراحی است.
یک سوال دیگر در این بحث مطرح میشود: وقتی عنوان میشود میزان اماده سازی زیاد است، زیاد یک صفت نسبی است، پس زیاد یعنی به چه میزان؟ وقتی در طرح این مشکل صفت زیاد عنوان میشود، به معنای بیش از نیاز است. نیاز یک تابع در حدی است که برای انجام وظیفه محوله کافی باشد. هر گونه آمادهسازی که به دلیل انتزاع غلط به وجود آمده است، اضافی است و باید از کد کم شود.
یک اصل در نوشتن تست وجود دارد: هر تابع تست، یک ادعا (assertion)
برای مثال فرض کنید تستی با عنوان زیر موجود است:
از عنوان این تست مشخص است که برای تابعی که ظاهرا وظیفه محاسبه مالیات را بر عهده دارد نوشته شده است. مشخصا برای این تست چندین ادعا باید نوشته شود که بتوان تمامی تابع مورد نظر را پوشش داد. این مورد را مقایسه کنید با تستهای زیر برای تابع مورد بحث:
نوشتن یک ادعا برای هر تابع، موجب بیشتر شدن کد اضافی در روندهای تست میشود، اما یک خروجی بسیار ارزشمند را نیز به همراه دارد: مستندات بسیار مناسب. یک مجموعه تست کافی با عناوین قابل فهم، یک منبع بسیار خوب برای آسنایی با توابع سیستم هستند.
راه حل برای مشکل مطرح شده، شکستن تست به چندین تست میبایشد که در هر تست تنها یک ادعا موجود باشد.
در روندهای نوشتن تست، از ماک برای شبیهسازی رفتار یک موجودیت خارجی استفاده میشود. این موجودیت خارجی شامل پایگاه داده، وب سرویس یا یک کلاس دیگر است که در تابع مورد تست استفاده شده است. برای مثال، تابعی وظیفه ارسال ایمیل در صورت ثبت نام کاربر را بر عهده دارد. در این شرایط میتوان سرویس ایمیل را ماک کرد! یعنی یک کدی مشابه سرویس ایمیل داشت که رفتار آن را تقلید کند.
ماک برای توسعه تست، یک نیاز اساسی است. اما در صورتی که تعداد زیاد ماک برای یک تست استفاده شود، نشانه یک مشکل در کد مورد تست است. تعداد زیاد ماک در نهایتا باعث مشکل عنوان شده قبلی (آمادهسازی زیاد) میشود.
فرض کنید تابعی به صورت زیر داریم:
به منظور تست تابع مورد اشاره، حداقل به سه ماک نیاز داریم: دیتابیس، سرویس ایمیل و سرویس لاگ. این تعداد ماک میتواند نشانه یک خطای طراحی در سیستم باشد که به GOD-object معروف است. در این شرایط بهتر است که تابعی که چندین وظیفه بر عهده دارد، به چند تابع کوچکتر شکسته شود.
راه حل: ابتدا تست را توسعه دهید، اگر موارد مشابه مشاهده شد، سعی کنید طراحی خود را تغییر دهید.
فرض کنید تابعی وظیفه دارد که رکورد ورودی را بررسی کرده و در صورت مشاهده رکورد دارای خطا، آن را به صورت لاگ گزارش کند. فرض کنید تعریف تابع به صورت زیر است:
بررسی اینکه مقدار خروجی True است یا False در این تست مشکلی ندارد. اما روند برای تست بهینه خروجی لاگ به چه صورت است؟ هر زبان برنامه نویسی یک مکانیزم مشخص برای تولید لاگ دارد. برای مثال در پایتون از logger استفاده میشود. در این مثال میتوان logger را mock کرد. اما سوال اصلی در این است که به چه صورت باید موجودیت mock شده از logger را به عنوان لاگر اصلی تابع در حال تست معرفی کرد. برای این روند ۲ راه حل وجود دارد:
راه حل اول قابل انجام است، اما باعث میشود که تست به پیادهسازی کتابخانههای مورد استفاده وابستگی زیادی داشته باشد. این وابستگی زیاد باعث میشود که با تغییر کوچک در کتابخانه (در اینجا logger) تست دچار مشکل شود.
شاید سوال پیش بیاید که تغییر کد به منظور فراهم آوردن تست بهینه، خارج از اصولی مانند Open-Close باشد. اما باید عنوان کرد که این مسائل در صورت پیادهسازی بهینه TDD مشاهده نمیشود. در صورتی که تست قبل از کد نوشته شود، وجود این مشکل به سادگی قابل تشخیص است و قبل از توسعه منطق کد، تغییرات مورد نظر اعمال میشودند.
این روند مشابه مشکل وصله کردن است. اما در این روند تست، سعی میشود در یک متغییر یا موجودیت داخلی یک تابع در حال تست تغییر (پچ کردن) انجام گیرد. برای مثال، تابع محاسبه مالیات که قبلا عنوان شد از یک مقدار آستانه استفاده میکند. این مقدار آستانه، به صورت یک مغییر داخلی تابع، کلاس یا گلوبال وجود دارد. در هنگام تست میتوان این متغییر را بر اساس نیاز، با پچ کردن تغییر داد و به هدف تست مورد نظر رسید.
برای یک مثال ملموستر، فرض کنید تابعی وجود دارد که وظیفه دارد در صورتی که روز جاری، روز کاری باشد مقدار True را برگشت دهد. تابع دیگر نیز بر اساس این مقدار هزینه غذا را اضافه میکند. تابع مورد بحث از توابع داخلی زبان برنامه نویسی برای تعیین روز هفته استفاده کرده و از یک سرویس تقویم برای تعیین تعطیل بودن یا نبودن استفاده میکند. فرض کنید امکان mock کردن سرویس تقویم وجود ندارد. در این شرایط میتوان مقدار متغیر داخلی تابع مورد بحث که مشخص کننده تعطیلی است را با پچ کردن تغییر داد.
اما این روش کاملا با اصول بسته بندی در OOP مخالف است. یعنی ما برای انجام یک تست، اصل دیگری را زیر پا گذاشتهایم. طراحی سیستم باید به صورتی انجام گیرد که با رعایت تمامی اصول OOP، قابلیت تست به صورت آسان را فراهم کند. مشخصا این مورد نیز در صورت نوشتن تست قبل از توسعه کد، قابل پیشگیری است.
در توسعه بر مبنای تست، اخلاق توسعه سازمانی و همکاری توسعهدهندهگان بسیار بیشتر اهمیت پیدا میکند. برخی از مواردی که در شرکتها قبل از استفاده از روند TDD حتما باید مورد بازنگری (و در صورت نبود ایجاد شود) عنوان میشوند:
در نهایت به عنوان نتیجه گیری، میتوان این جمله را عنوان کرد: تست مناسب رفتار یک سیستم را مورد بررسی قرار میدهد و نه پیادهسازی آن را، و هر اندازه نوشتن تست به تاخیر بیافتد از ارزش آن کاسته میشود.
در پستهای بعدی به بررسی BDD که روندی برای بهبود قسمت اول جمله قبلی هست، پرداخته خواهد شد و تجربیاتی از آن عنوان خواهد شد.