۳۰ روز با TDD: روز چهارم - Pass کردن اولین تست

نوشته زبان انگلیسی را در این آدرس می‌توانید مشاهده کنید. در روز سوم، اولین تست‌مان را نوشتیم. هدف این بود:

“یک متد بنویسید که یک جمله و یک کاراکتر را به عنوان ورودی دریافت کند و عددی را برگرداند که مشخص کند آن کاراکتر چند بار در آن جمله استفاده شده است”

کدی در پایان نوشته روز سوم، داشتیم کد زیر بود. در این کد فقط متد مربوط به تست را نوشتیم و امروز کاری خواهیم کرد که این تست Pass شود.

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

اگر من یک تست برای یک قابلیت بنوسیم که نباید در برنامه من باشد، این کار چند چیز به من خواهد گفت. اول از همه کمک می‌کند که تایید (و نه تضمین) کنم که تست درستی نوشتم. اگر تستی بنویسم و بلافاصله پس از نوشتن، اجرا و Pass شود، احتمالاً من قابلیت درستی را آزمایش نمی‌کنم چرا که آن قابلیت نباید وجود داشته باشد و تست نباید Pass‌ شود.
این مورد الان و در مورد مثال ما با توجه به ساده بودن مثال و تست و کد شاید مسخره به نظر برسد، اما وقتی روی یک پروژه بزرگ و پیچیده شروع به کار کنید، بعضی اوقات نوشتن یک تست که نبود یک feature را آشکار کند کار دشواری است.

نتیجه اولین دلیل برای اینکه تست را اجرا و fail‌ شدنش را ببینیم این است که مطمئن شوم کار یک نفر دیگر را دوباره تکرار نمی‌کنم. بعضی اوقات به خصوص در پروژه‌های بزرگ، امکان دارد که یک قابلیت به نحوی و به اشتباه دو بار برای اجرا برنامه‌ریزی شود. در این شرایط Pass شدن تست مشخص می‌کند که من در حال اجرای مجدد کاری هستم که یک نفر قبلاً انجامش داده است. دقت نکردن در این مورد می‌تواند منجر به تولید یک کد ناخوانا، غیرقابل اطمینان و غیرقابل نگهداری شود که هیچ‌کس دوست ندارد با آن کار کند.

قبل از اجرای تست لازم است که solution را کامپایل کنم و اینجاست که بلافاصله اولین خطا را می‌توان دید:

بر اساس تعریف **اگر solution شما در Visual Studio کامپایل نشود، تست شما fail‌ محسوب می‌شود.**در این مثال، ریشه خطا برمی‌گردد به کلاس StringUtils که در خط 19 کد تلاش کردم یک instance از آن ایجاد کنم در حالی که نمونه‌ای از آن در کد وجود ندارد.

حالا برمی‌گردیم به مسیر TDD، جایی که باید کلاس‌هایی که در حال تست‌شان هستیم (در مورد این مثال کلاس StringUtils) را بسازیم. اینجا دو گزینه پیش‌رو داریم: یا باید کلاس را داخل همین پروژه فعلی بسازیم (بعضی برنامه‌نویس‌ها حتی آن را داخل فایل کلاس تست می‌سازند) یا یک پروژه Class Library جدید ایجاد و در آن کلاس مورد نظر را بسازیم.

در مورد راه اول، ابتدا تاکید می‌کنم که ایده اصلی TDD این است که یک قابلیت را تا زمانی که به آن نیازی نیست پیاده‌سازی نکنید. این کمک می‌کند که کد شما تمیز مانده و به دور از کدهای درهم و برهمی باشد که فهمیدن منطق و کد برنامه شما را از آن چیزی که باید،‌ سخت‌تر می‌کند. بر این اساس تا زمانی که به یک قابلیت نیاز نداشته باشید آن را به برنامه اضافه نمی‌کنید و وقتی به یک کلاس احتیاج دارید آن را به پروژه‌ای در Visual Studio که آن کلاس باید جزئی از آن باشد انتقال می‌دهید.
اما اگر راه اول را برویم، در بعضی اوقات ممکن است انتقال کد از پروژه تست به پروژه اصلی فراموش شود و این یعنی دوباره‌کاری برای برنامه‌نویس‌ها. به هر حال شما بر اساس شرایط خود و شرایط پروژه‌تان راهی که بهتر هست را انتخاب کنید.

با توجه به توضیحات بالا، ما یک کلاس جدید (در یک فایل جدید) در پروژه جاری ایجاد می‌کنیم و بعداً هم اگر لازم باشد آن را به پروژه دیگری انتقال بدهیم به راحتی می‌توانیم این کار را انجام دهیم. البته این تمیز‌ترین راه نیست اما ساده‌ترین و سریع‌ترین است.

توضیح مترجم: نویسنده مطلب اصلی، از برنامه تلریکی JustCode برای ایجاد کلاس یا اجرای تست‌ها و … استفاده کرده و به نوعی تبلیغ این محصول را هم در نوشته‌های مختلفش آورده است. من برای اینکه سری نوشته‌های 30 روز با TDD برای عموم دوستان برنامه‌نویس قابل استفاده باشد، از ابزارهای غیراختصاصی و عمدتاً رایگان در دسترس استفاده خواهم کرد.

برای ایجاد یک کلاس جدید روی پروژه کلیک راست کرده و از منوی Add گزینه کلاس را انتخاب کنید و نام StringUtils را به آن بدهید. کلاس را public کنید. تا کدی مشابه کد زیر داشته باشید:

ساختن کلاس ساده‌ترین کاری است که ممکن است منجر به Pass‌ شدن تست من شود، یا حداقل خطای کامپایل قبلی را برطرف کند. وقتی دوباره تلاش می‌کنم که برنامه را کامپایل کنم خطای زیر را می‌بینم:

بر اساس خطا، کار بعدی که باید انجام دهم اضافه کردن تعریف متد FindNumberOfOccurneces  به کلاس StringUtils است. این کار را انجام می‌دهم اما در داخل بدنه متد، کد خاصی نمی‌نویسم. نتیجه به صورت زیر خواهد بود:

پیشنهاد مترجم: ابزارهای مختلفی برای اجرا تست‌ها وجود دارند، اگر به دنبال ابزار رایگان هستید، استفاده از نسخه رایگان TestDriven.NET را پیشنهاد می‌کنم. TestDriven.NET از NUnit پشتیبانی می‌کند و بعد از نصب، برای اجرا تست‌ها کافی است روی پروژه کلیک راست کرده و گزینه Run Tests را انتخاب کنید. اگر به دنبال ابزارهای غیررایگان هستید Resharper به گمان من بهترین باشد. شما هم می‌توانید ابزارهای مورد نظر خود را در بخش نظرات معرفی کنید.

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

خب چه اتفاقی افتاد؟ تست fail شد. چرا که ما در متد FindNumberOfOccurences هیچ کدی ننوشته یک exception را ایجاد کردیم. بسته به ابزاری که برای اجرا کردن تست‌ها انتخاب می‌کنید،‌ می‌توانید خطای مربوط به این exception را مشاهده کنید.

قدم بعدی این است که ساده‌تری راه را برای اینکه تست Pass شود انتخاب و در کد پیاده‌سازی کنیم. هدف تست ما این است که در جمله “!TDD is awesome” تعداد کاراکترهای “e” را پیدا کنیم و خروجی مورد انتظار ما 2 است، پس اجازه بدهید کد را به صورت زیر بازنویسی کنیم:

در کد بالا خیلی راحت عدد 2 را return می‌کنیم. حالا دوباره تست را اجرا کنید. نتیجه؟ تست Pass‌ می‌شود، هورا! نرم‌افزار آماده عرضه است!

خب، واضح است که برنامه ما هنوز قابل عرضه نیست. منظور ما از پیاده‌سازی قابلیت مورد نظر، برگرداندن عدد دو به صورت hardcode شده در کد نبود. اما اگر تستی که نوشتیم، تنها تست موجود برای این قابلیت بود، می‌توانستیم بگوییم کار تمام شده است و مهمتر اینکه دیگر develop بیشتری در این کد انجام نمی‌دادیم. به عنوان برنامه‌نویس گاهی وقت‌ها سخت است که متوجه شویم چه زمانی دیگر کافی است: بعضی اوقات قابلیت‌هایی در کد اضافه می‌کنیم که در لیست مشخصات نرم‌افزار نیست چرا که فکر می‌کنیم “آن‌ها الان نمی‌دانند، ولی بعداً به این نیاز خواهند داشت”

خب، اگر واقعاً به آن قابلیت احتیاج نداشته باشند چی؟ برای برنامه‌نویس‌ها قرار گرفتن در موقعیت “من بهتر از کاربران می‌فهمم” چیز تازه‌ای نیست. اما در حالی که ما در زمینه توسعه نرم‌افزار متخصص هستیم، معمولاً در زمینه‌هایی مثل تایید کردن وام یا ارزیابی اطلاعات پزشکی یا تفسیر اطلاعات فروش، تخصصی نداریم. همان‌طور که ما دوست نداریم یک نفر از بخش حسابداری یا فروش به ما بگوید که چطور کد بنویسیم، ما هم نباید به آن‌ها بگوییم که برای انجام کارشان به چه چیزی نیاز دارند.
بازار می‌داند که تجارت چیست و چه چیزی لازم دارد و اگر نداند هم بالاخره خواهد فهمید و آن را تقاضا خواهد کرد. ما فقط باید منتظر بمانیم که آن‌ها نیازشان را مطرح کنند و دقیقاً همان چیزی را که می‌خواهند به آن‌ها بدهیم. تلاش برای “پیش‌بینی” نیاز کاربران، باعث ایجاد کدها و قابلیت‌های بسیار زیادی در سیستم می‌شود که هرگز استفاده نمی‌شوند و هیچ ارزشی را به سیستم اضافه نمی‌کنند.

نتیجه اخلاقی اینکه اجازه بدهید بازار (bussiness) تصمیم بگیرد که چه چیزی نیاز است. دقیقاً همان چیزی که احتیاج دارند را تولید کنید، نه بیشتر نه کمتر. اگر نگران این هستید که کاربران نیازهای غیرمنطقی مطرح کنند، با آن‌ها صحبت کنید. اما به یاد داشته باشید که در نهایت برنامه‌ای که ایجاد می‌کنید قرار است پاسخگوی نیاز آن‌ها باشد. اگر این کار کمک نکرد، هر نیاز جدید را به پول وصل کنید، واقعاً شگفت‌انگیز است که چطور تطبیق نوشتن کد با خرج کردن پول،‌ می‌تواند scope پروژه را کنترل کند ;)

برگردیم به مثال خودمان. در مثال ما، می‌توان گفت که test case موجود کافی نیست. برای بهبود کیفیت کد، ما یک تست جدید اضافه می‌کنیم:

من می‌خواهم جمله “Once is unique, twice is a coincidence, three times is a pattern” را به همراه کاراکتر “n” به متد پاس بدهم و خروجی مورد انتظار هم عدد 5 است.

گام بعدی نوشتن یک تست جدید است. اسم متد تست را ShouldBeAbleToCountNumberOfLettersInAComplexSentence می‌گذارم و آن را به کلاس StringUtilsTest به شکل زیر اضافه می‌کنم:

حالا که تست را نوشتیم، بلافاصله آن را اجرا می‌کنیم تا fail شدنش را ببینیم. تست fail می‌شود.

آخرین باری که تست fail شده داشتیم برای زمانی بودن که متد کلاسی که در حال تست آن بودیم پیاده‌سازی (implement) نشده بود. این بار متد، پیاده‌سازی شده است، اما نتیجه‌ای که می‌خواهیم (عدد 5) را نمی‌گیریم. بسته به اجرا کننده تست،‌ خطای Expected: 5 But was: 2 را خواهید دید.

ساده‌ترین راه برای اینکه تست قبلی Pass شود این بود که خیلی راحت عدد دو را برگردانیم. این بار فکر می‌کنم ساده‌ترین راه این باشد که یک الگوریتم که تعداد یک کاراکتر را داخل یک جمله پیدا کند را پیاده‌سازی کنیم. این الگوریتم را در متد FindNumberOfOccurences در کلاس StringUtils به شکل زیر پیاده‌سازی می‌کنیم:

این ممکن است بهینه‌ترین یا بهترین راه حل مساله نباشد، ولی الان مهم نیست: این ساده‌ترین است و من دنبال ساده‌ترین راه برای Pass کردن تست‌هایم می‌گردم. اگر دوباره تست‌ها را اجرا کنیم می‌بینیم که الگوریتم، پاسخگویی نیازمندی ما بوده است و بنابراین هر دو تست Pass می‌شوند.

در آینده دوباره این مثال را با هم بررسی می‌کنیم تا درباره چند مبحث پیشرفته TDD و Refactoring با هم صحبت کنیم. ولی الان من می‌دانم که صرفنظر از اینکه بعداً با کد چه کار خواهم کرد، تا زمانی که این دو تست Pass شوند، کد من همچنان پاسخگوی این دو test case خواهد بود.

ادامه دارد…