۳۰ روز با TDD: روز هشتم: برخورد با defect ها

نوشته به زبان انگلیسی روز هشتم را در این آدرس می‌توانید مطالعه کنید. قبل از شروع لازم است درباره موضوع امروز یعنی defect و تفاوت آن با bug نکته‌ای را عرض کنم. defect که شاید بتوان آن را نقص ترجمه کرد در واقع انحراف از نیازمندی‌های نرم‌افزار است و bug نتیجه خطا در کدنویسی، اینجا را ببینید.

defect بد در نرم‌افزار خوب

مهم نیست که چقدر خوب کد بنویسید و چقدر پیاده‌سازی نیازمندی‌های اصلی را دنبال کرده‌اید و مهم نیست که چقدر نرم‌افزارتان را تست کرده‌اید، defect ها یک واقعیت در دنیای نرم‌افزار هستند. البته TDD به شما کمک می‌کند که تعداد آن‌ها را تا حد زیادی کم کنید ولی فکر می‌کنم راهی برای اینکه از defect ها اجتناب کنیم وجود نداشته باشد.

شاید دلیل defect ها، نیازمندی‌های تدوین شده برای نرم‌افزار باشد که دقیقاً مطابق نیاز تجاری نیست. شاید فهم نادرست یا ناقص از نیازمندی‌های نرم‌افزار باشد و یا شاید یک جنبه فنی از طراحی نرم‌افزار باشد که شما خیلی راحت آن را جا انداخته‌اید. صرفنظر از دلیل، اگر نرم‌افزار تولید می‌کنید، defect ها یک حقیقت در زندگی شما هستند. خبر خوب این است که اگر TDD را تمرین می‌کنید گردش کار برای مقابله با defect ها نه تنها ساده است، بلکه اگر درست اجرایی شود می‌تواند به اطمینان از اینکه defect های حل شده، حل شده باقی بمانند کمک می‌کند.

برای نشان دادن این موضوع، اجازه بدهید برگردیم و مثال روز سوم و چهارم را با هم مرور کنیم. برای یادآوری صورت مساله، قرار بود من یک متد بنویسیم که تعداد تکرار یک کاراکتر در یک رشته را بشمارد. برای این کار من تست‌های زیر را نوشتم

الگوریتمی که برای pass کردن تست‌های بالا استفاده کردم را در زیر می‌توانید مشاهده کنید

بعد از اینکه مدتی از نوشتن این کدها گذشت، defect زیر گزارش شد:

کاربر قادر است که ۲ کاراکتر را به عنوان پارامتر دوم به تابع FindNumberOfOccurences پاس دهد و بعد از این کار یک exception از نوع FormatException ایجاد می‌شود. رفتار مورد انتظار این است که متد استثنا از نوع ArgumentException برگرداند.

وقتی به این موضوع فکر کنید، defect ها در واقع یک نوع دیگر نیازمندی نرم‌افزار هستند. نیازمندی‌های سنتی نرم‌افزار می‌گویند که یک نرم‌افزار چطور باید کار کند در حالی که defect‌ها می‌گویند نرم‌افزار چطور باید کار می‌کرده است. وقتی شما از TDD استفاده می‌کنید و نرم‌افزارتان را بر پایه تست‌ها می‌نویسید که خود آن تست‌ها بر پایه نیازمندی‌های نرم‌افزار تولید شده‌اند، شما باید همه آن نیازمندی‌ها را در کد خود ببینید. اگر این درست باشد، defect ها چیزی به جز یک نیازمندی جدید که شما نمی‌دانستید وجود دارد یا بهبود یک نیازمندی پیاده شده فعلی نیستند. این نیازمندی‌ها (در واقع defect ها) می‌توانند کاربردی باشند (مثل اینکه برنامه در IE 6 کار نمی‌کند) یا مانند مساله بالا، مربوط به حوزه نیازمندی‌های تجاری (business domain requirement) باشند.

صرفنظر از نوع نیازمندی‌ جدید، گردش کار مربوط به آن مشابه است. اول باید یک تست بنویسم. در مورد این مثال من تستی می‌نویسم که مشخص کند که رفتار اشاره شده در شرح defect در واقع یک خطا (error) است و بنابراین تست باید fail شود

این شبیه تست قبلی به نظر می‌رسد اما چند تفاوت وجود دارد. یکی از آن‌ تفاو‌ت‌ها این است که من نوع مقدار برگشتی از FindNumberOfOccurences را مشخص نکردم و حتی مقدار برگشتی را دریافت یا ذخیره نکردم و چون هیچ‌یک از این دو را ندارم هیچ فراخوانی Assert.AreEqual هم انجام نشده است. این مساله به این خاطر است که من انتظار دارم که تابع FindNumberOfOccurences یک استثنا (exception) از نوع  ArgumentException ایجاد کند.

انتظار دارم که در اولین اجرا، تست fail شود و نتیجه زیر مرا ناامید نمی‌کند:

اگر گردش کار TDD را به یاد داشته باشید، می‌دانید که گام بعدی نوشتن ساده‌ترین کدی است که کار می‌کند. برای این مثال من پیاده‌سازی تابع FindNumberOfOccurences را تغییر می‌دهم تا بلافاصله یک exception ایجاد کند.

همان‌طور که در خط ۹ می‌بینید من خیلی ساده یک وهله (instance) جدید از نوع ArgumentException ایجاد و throw کرده‌ام. اگر تستم را دوباره اجرا کنم خواهم دید که تست pass می‌شود.

تست جدید pass می‌شود. قبل از اینکه بگوییم کارمان تمام شده باید مطمئن شویم که چیزی خراب نشده باشد. این کار را با اجرا کردن همه تست‌ها می‌توانیم انجام دهیم.

در حالی که ما defect را fix کردیم چیز دیگری را دچار مشکل کردیم. این یک مثال خوب درباره اهمیت TDD و Unit Test به صورت کلی است. بدون وجود تست‌ها، هیچ راه ساده‌ای برای فهمیدن اینکه با تغییر تابع چه تاثیری در کد گذاشته شده است وجود نداشت. در این مورد در نوشته‌های بعدی که درباره refactoring هستند بیشتر صحبت می‌کنیم.

حالا باید کاری کنم که تمام تست‌ها pass‌ شوند. این کار با تغییری در تابع FindNumberOfOccurences انجام می‌شود

در این مثال، کل کد را در یک بلوک try/catch گذاشتم. اگر exception ای رخ بدهد آن را قورت می‌دهم و به جایش یک exception از نوع ArgumentException ایجاد و throw می‌کنم. می‌دانم این راه حل خوبی نیست اما نگران نباشید. فعلاً این کد پاسخگوی نیاز تست من است، در آینده وقتی درباره refactoring صحبت کنیم، برمی‌گردیم و این کد را دوباره با هم مرور خواهیم کرد.

با اجرای مجدد تست‌ها، می‌توانیم ببینیم که نه تنها defect مرتفع شده بلکه کلیه کاربردهای قبلی هم مثل قبل درست کار می‌کنند.

ادامه دارد...