بیش از ده سال از زمانی میگذرد که توسعهٔ آزمونمحور (TDD) وارد صنعت شد. این رویکرد بهعنوان بخشی از موج برنامهنویسی افراطی (XP) معرفی شد، اما از آن زمان توسط اسکرام و تقریباً همهٔ روشهای چابک دیگر پذیرفته شده است. حتی تیمهایی که چابک هم نیستند، TDD را به کار میگیرند.
وقتی در سال ۱۹۹۸ برای نخستین بار دربارهٔ «برنامهنویسی آزموناول» شنیدم، بدبین بودم. چه کسی بدبین نمیشد؟ اول تست واحد را بنویسی؟ چه کسی چنین کار احمقانهای میکند؟
اما آن زمان سی سال بود که برنامهنویس حرفهای بودم و چیزهای زیادی را دیده بودم که آمدهاند و رفتهاند. میدانستم نباید چیزی را بیدرنگ رد کنم، مخصوصاً وقتی کسی مثل کنت بک آن را مطرح میکند.
پس در سال ۱۹۹۹ به مدفوردِ اورگن رفتم تا با کنت ملاقات کنم و این انضباط را مستقیماً از خودش یاد بگیرم. کل تجربه شوکهکننده بود!
کنت و من در دفترش نشستیم و شروع کردیم به کدنویسی یک مسئلهٔ ساده در جاوا. من میخواستم یکراست بروم سراغ نوشتن همان کد ساده. اما کنت مقاومت کرد و مرا قدمبهقدم از فرایند عبور داد. اول بخش کوچکی از یک تست واحد را نوشت؛ آنقدر کوچک که بهسختی میشد گفت کد است. بعد فقط به اندازهای کد تولید نوشت که آن تست کامپایل شود. بعد کمی تست دیگر، و بعد کمی کد دیگر.
زمان چرخه کاملاً بیرون از تجربهٔ من بود. من عادت داشتم یک ساعت تمام کد بنویسم و بعد تازه سعی کنم آن را کامپایل یا اجرا کنم. اما کنت عملاً هر سی ثانیه یکبار کدش را اجرا میکرد. مبهوت شده بودم!
از آن عجیبتر اینکه این زمان چرخه برایم آشنا بود! همان نوع چرخهای که سالها قبل، وقتی بچه بودم، با زبانهای تفسیری مثل بیسیک یا لوگو برای نوشتن بازیها استفاده میکردم. در آن زبانها زمان ساخت وجود ندارد؛ یک خط کد اضافه میکنی و اجرا میکنی. چرخه خیلی سریع میچرخد و به همین دلیل میتوانی بسیار پربازده باشی.
اما در «برنامهنویسی واقعی» چنین زمان چرخهای مسخره به نظر میرسید. در برنامهنویسی واقعی باید مدت زیادی کد بنویسی، بعد مدت بیشتری صرف کامپایل کنی، و بعد باز هم وقت زیادی برای دیباگ. من برنامهنویس ++C بودم، لعنتی! و در ++C زمانهای بیلد و لینک به دقیقهها—گاهی ساعتها—میرسید. چرخههای سیثانیهای غیرقابل تصور بود.
با این حال، کنت آنجا بود؛ داشت با همان چرخههای سیثانیهای در جاوا کد میپخت و هیچ نشانهای هم از کند شدن نداشت. همانجا، در دفتر کنت، به این نتیجه رسیدم که با این انضباط ساده میتوانم در زبانهای «واقعی» با زمان چرخهٔ لوگو کدنویسی کنم! همانجا گیر افتادم.
از آن روزها فهمیدهام که TDD بسیار فراتر از یک ترفند ساده برای کوتاه کردن زمان چرخه است. این انضباط مجموعهای کامل از مزایا دارد که در ادامه شرح میدهم.
اما اول باید این را بگویم:
- رأی نهایی صادر شده است!
- بحثها تمام شدهاند.
- دستور GOTO مضر است.
- و TDD کار میکند.
بله، در طول سالها وبلاگها و مقالههای بحثبرانگیز زیادی دربارهٔ TDD نوشته شده و هنوز هم نوشته میشود. در روزهای اول، اینها تلاشهای جدی برای نقد و فهم بودند. اما امروز بیشترشان فقط غر زدناند. خطِ آخر این است: TDD کار میکند و همه باید با این واقعیت کنار بیایند.
میدانم این حرف تند و یکطرفه به نظر میرسد، اما با توجه به سابقه، فکر نمیکنم جراحها مجبور باشند شستن دستها را توجیه کنند، و فکر نمیکنم برنامهنویسها مجبور باشند از TDD دفاع کنند.
چطور میتوانید خودتان را حرفهای بدانید اگر ندانید همهٔ کدتان کار میکند؟ چطور میتوانید بدانید همهٔ کدتان کار میکند اگر هر بار که تغییری میدهید آن را تست نکنید؟ چطور میتوانید هر بار تست کنید اگر تستهای واحد خودکار با پوشش بسیار بالا نداشته باشید؟ و چطور میتوانید به تستهای خودکار با پوشش بالا برسید اگر TDD را تمرین نکنید؟
جملهٔ آخر نیاز به توضیح دارد. اصلاً TDD چیست؟
۱. اجازه ندارید هیچ کد تولیدی بنویسید مگر اینکه ابتدا یک تست واحدِ شکستخورده نوشته باشید. ۲. اجازه ندارید بیش از آنچه برای شکست خوردن لازم است تست واحد بنویسید—و کامپایل نشدن هم شکست است. ۳. اجازه ندارید بیش از آنچه برای پاس شدن تست واحدِ شکستخوردهٔ فعلی لازم است کد تولید بنویسید.
این سه قانون شما را در چرخهای قفل میکنند که شاید سی ثانیه طول بکشد. با نوشتن بخش کوچکی از یک تست واحد شروع میکنید. اما ظرف چند ثانیه مجبور میشوید نام کلاسی یا تابعی را صدا بزنید که هنوز وجود ندارد، و همین باعث میشود تست کامپایل نشود. پس باید کد تولیدی بنویسید تا تست کامپایل شود. اما نمیتوانید بیشتر از آن بنویسید، پس دوباره به نوشتن تست برمیگردید.
دورِ چرخه همینطور میچرخد: کمی تست، کمی کد تولید. این دو جریان کد همزمان رشد میکنند و به اجزایی مکمل تبدیل میشوند. تستها مثل پادتن، دقیقاً به پادگنِ کد تولید میچسبند.
اگر TDD را بهعنوان یک انضباط حرفهای بپذیرید، هر روز دهها تست خواهید نوشت، هر هفته صدها تست، و هر سال هزاران تست. و همهٔ این تستها را نگه میدارید و هر بار که تغییری در کد میدهید اجرا میکنید.
من نویسنده و نگهدارندهٔ اصلی FitNesse هستم؛ یک ابزار تست پذیرش مبتنی بر جاوا. در زمان نگارش این متن، FitNesse حدود ۶۴ هزار خط کد دارد که ۲۸ هزار خط آن در بیش از ۲۲۰۰ تست واحد قرار گرفته است. این تستها دستکم ۹۰٪ کد تولید را پوشش میدهند و اجرایشان حدود ۹۰ ثانیه طول میکشد.
هر وقت تغییری در هر بخش از FitNesse میدهم، کافی است تستها را اجرا کنم. اگر پاس شوند، تقریباً مطمئنم تغییری که دادهام چیزی را نشکسته است. «تقریباً مطمئن» یعنی چقدر؟ آنقدر که منتشرش کنم!
فرایند QA برای FitNesse یک دستور است: ant release. این دستور FitNesse را از صفر میسازد و بعد همهٔ تستهای واحد و پذیرش را اجرا میکند. اگر همه پاس شوند، منتشر میکنم.
FitNesse یک نرمافزار مأموریتحیاتی نیست. اگر باگی داشته باشد، کسی نمیمیرد و کسی میلیونها دلار از دست نمیدهد. پس میتوانم فقط بر اساس پاس شدن تستها منتشر کنم. با این حال، FitNesse هزاران کاربر دارد و با وجود اضافه شدن ۲۰ هزار خط کد جدید در سال گذشته، فهرست باگهای من فقط ۱۷ مورد است (که بسیاریشان ظاهریاند). پس میدانم نرخ تزریق نقص من بسیار پایین است.
این اثر استثنایی نیست. گزارشها و مطالعات متعددی کاهش چشمگیر نقص را نشان دادهاند. از IBM تا Microsoft، از Sabre تا Symantec، شرکت پشت شرکت و تیم پشت تیم کاهش نقصهای ۲ برابر، ۵ برابر و حتی ۱۰ برابر را تجربه کردهاند. این اعدادی نیستند که یک حرفهای بتواند نادیده بگیرد.
چرا وقتی کد بد میبینید آن را اصلاح نمیکنید؟ واکنش اولتان این است: «این بههمریخته است، باید تمیز شود.» واکنش دوم: «دست نمیزنم!» چرا؟ چون میدانید اگر دست بزنید، خطر خراب کردنش هست؛ و اگر خرابش کنید، مالِ شما میشود.
اما اگر میتوانستید مطمئن باشید که تمیزکاریتان چیزی را نمیشکند چه؟ اگر همان قطعیتی را داشتید که گفتیم؟ اگر میتوانستید دکمهای بزنید و ظرف ۹۰ ثانیه بفهمید که تغییراتتان فقط خوب بوده و چیزی را خراب نکرده؟
این یکی از قدرتمندترین مزایای TDD است. وقتی به مجموعه تستهایتان اعتماد دارید، ترس از تغییر از بین میرود. کد بد را همانجا تمیز میکنید. کد به گل رس تبدیل میشود که میتوانید با خیال راحت به شکلهای ساده و دلپذیر درآورید.
وقتی برنامهنویسها ترس از تمیزکاری را از دست میدهند، تمیزکاری میکنند! و کد تمیز فهمیدنش آسانتر است، تغییر دادنش آسانتر است و توسعهاش آسانتر. نقصها کمتر میشوند چون کد سادهتر میشود. و کدبیس بهجای پوسیدنِ تدریجی—که صنعت به آن عادت کرده—بهطور پیوسته بهتر میشود.
کدام برنامهنویس حرفهای اجازه میدهد این پوسیدگی ادامه پیدا کند؟
تا به حال از یک فریمورک شخص ثالث استفاده کردهاید؟ معمولاً یک راهنمای خوشفرم به شما میدهند که نویسندگان فنی نوشتهاند. راهنمای معمول شامل ۲۷ عکس رنگی براقِ قطع بزرگ با دایره و فلش است و پشت هر کدام پاراگرافی دربارهٔ پیکربندی، استقرار و استفاده. آخرش هم یک پیوست زشت دارد که مثالهای کد آنجاست.
اولین جایی که میروید کجاست؟ اگر برنامهنویس باشید، میروید سراغ مثالهای کد. چون میدانید کد حقیقت را میگوید. آن عکسهای براق شاید قشنگ باشند، اما برای فهمیدن نحوهٔ استفاده باید کد بخوانید.
هر تست واحدی که با پیروی از سه قانون مینویسید، یک مثال است—مثالی که با کد نوشته شده و توضیح میدهد سیستم چگونه باید استفاده شود. اگر سه قانون را رعایت کنید، برای هر شیء در سیستم تستی خواهید داشت که نشان میدهد چطور ساخته میشود، و برای هر تابع تستی که نشان میدهد چطور و در چه حالتهایی باید صدا زده شود. برای هر کاری که بخواهید انجام دهید، یک تست واحد هست که آن را با جزئیات شرح میدهد.
تستهای واحد سند هستند. آنها طراحی سطح پایین سیستم را توصیف میکنند. بدون ابهاماند، دقیقاند، به زبانی نوشته شدهاند که مخاطب میفهمد، و آنقدر رسمیاند که اجرا میشوند. این بهترین نوع مستندسازی سطح پایین است. کدام حرفهای چنین مستنداتی را ارائه نمیدهد؟
وقتی طبق سه قانون و با نوشتن تستها از ابتدا پیش میروید، با یک دوراهی روبهرو میشوید. اغلب دقیقاً میدانید چه کدی میخواهید بنویسید، اما قوانین میگویند باید اول تستی بنویسید که چون آن کد وجود ندارد، شکست بخورد! یعنی باید کدی را تست کنید که هنوز ننوشتهاید.
مشکل تست کردن کد این است که باید آن را ایزوله کنید. اگر تابعی توابع دیگر را صدا بزند، تستش سخت میشود. برای نوشتن تست مجبورید راهی برای جدا کردن تابع از بقیه پیدا کنید. به بیان دیگر، نیاز به تستاول شما را مجبور میکند به طراحی خوب فکر کنید.
اگر تستها را اول ننویسید، نیرویی وجود ندارد که جلوی چسباندن توابع به یک تودهٔ غیرقابلتست را بگیرد. اگر بعداً تست بنویسید، شاید بتوانید ورودی و خروجی کل توده را تست کنید، اما تست تکتک توابع بسیار دشوار خواهد بود.
پس پیروی از سه قانون و نوشتن تستها از ابتدا نیرویی ایجاد میکند که شما را به طراحی جداافتادهتر و بهتر سوق میدهد. کدام حرفهای از ابزارهایی که او را به طراحی بهتر هدایت میکنند استفاده نمیکند؟
«اما میتوانم تستها را بعداً بنویسم.» نه، واقعاً نمیتوانید. میتوانید چند تست بعداً بنویسید. حتی اگر پوشش را دقیق اندازه بگیرید، شاید به پوشش بالا هم برسید. اما تستهایی که بعداً نوشته میشوند دفاعیاند. تستهایی که اول نوشته میشوند تهاجمیاند. تستهای بعد از واقعیت را کسی مینویسد که از قبل درگیر کد است و میداند مسئله چگونه حل شده. چنین تستهایی هرگز به تیزی تستهای اول نخواهند بود.
خلاصهٔ کلام این است که TDD انتخاب حرفهای است. انضباطی است که قطعیت، شجاعت، کاهش نقص، مستندسازی و طراحی را بهبود میدهد. با این همه مزیت، میتوان گفت استفاده نکردن از آن غیرحرفهای است.
با همهٔ خوبیهایش، TDD نه دین است و نه فرمول جادویی. پیروی از سه قانون هیچکدام از این مزایا را تضمین نمیکند. حتی اگر تستها را اول بنویسید، باز هم میتوانید کد بد بنویسید. حتی میتوانید تست بد بنویسید.
همچنین مواقعی وجود دارد که پیروی از سه قانون عملاً ناممکن یا نامناسب است. این موارد نادرند، اما وجود دارند. هیچ توسعهدهندهٔ حرفهای نباید از انضباطی پیروی کند که در آن شرایط خاص، بیشتر ضرر دارد تا فایده.
- Maximilien & Williams، «ارزیابی توسعهٔ آزمونمحور در IBM»
- George & Williams، «بررسی اولیهٔ TDD در صنعت»
- Janzen & Saiedian، «مفاهیم، ردهبندی و آیندهٔ TDD»
- Nagappan و همکاران، «بهبود کیفیت از طریق TDD در تیمهای صنعتی»
