چکیده: TDD یک رویکرد توسعه نرمافزار است که در آن یک تست قبل از نوشتن کد نوشته میشود. وقتی TDD به درستی راهاندازی شود، میتواند مزایای بیشماری به همراه داشته باشد و به یک عامل قدرتمندِ صرفهجویی در هزینه تبدیل شود و یک ارزش واقعی برای کسب و کار فراهم نماید. وقتی TDD به درستی ستاپ نشده باشد یا بدون درک نحوه استفاده از آن ستاپ شده باشد، میتواند به شکل بدی منجر به اتلاف وقت و هزینه شود. کیفیت نه از طریق بازرسی بلکه از طریق بهبود فرآیند تولید حاصل میشود.
آیا می دانید:
- ما ۱۰ برابرِ زمانِ نوشتن کد را برای خواندن کد صرف میکنیم، زیرا برای نوشتن یک کد جدید باید بدانید که کد قبلی چه میکند (کتاب مشهور Robert C. Martin، Clean Code: A Handbook of Agile Software Craftsmanship).
- ۶۲۰ میلیون نفر ساعت برنامهنویسی در سال با هزینهای در حدود ۶۱ میلیارد دلار، برای دیباگ کردن Failureهای نرمافزار هدر میرود(مطالعه جدید توسط Cambridge Judge Business School).
- مهندسین نرمافزار به طور متوسط ۱۳ ساعت برای رفع یک Failure نرمافزاری وقت صرف میکنند(مطالعه جدید توسط Cambridge Judge Business School).
- هزینه رفع Bugهای موجود در مرحله تست میتواند ۱۵ برابر بیشتر از هزینه رفع آنها در هنگام اجرا/طراحی باشد(کتاب Capers Jones, Applied Software Measurement: Global Analysis of Productivity and Quality).
برای رفع این چالشها، توسعه تست محور یا Test Driven Development یا همان TDD وارد عمل میشود. TDD یک رویکرد توسعه نرمافزار است که در آن یک تست در سطح یونیت قبل از نوشتن کد، نوشته میشود. هنگامی که کد جدید با موفقیت از تست عبور کرد، به استانداردهای قابل قبول Refactor میشود. به طور کلی، در این راستا مراحل زیر به ترتیب دنبال میشوند:
- یک تست اضافه کنید
- همه تست ها را اجرا کنید و ببینید که آیا تست جدید با شکست مواجه میشود یا خیر
- مقداری کد بنویسید
- تستها را اجرا کنید
- کد را ریفرکتور کنید
- مراحل ۱ تا ۵ را تکرار کنید
این روش در نوشتن تستهای ناموفق(تستهای Fail شونده)، معروف به چرخه “قرمز-سبز-ریفکتور” است که بر ایجاد عملیاتی قابل توصیف و قابل تست تمرکز دارد. تصویر این چرخه به عنوان عکس این مقاله در بالا ارائه شده است.
در حقیقت شما قبل از نوشتن هر کدی باید به خوبی فکر کنید که کد شما قرار است چه کاری انجام دهد تا بتوانید کد تست کننده آنرا به عنوان Unit Test Case طراحی کنید. پس از نوشتن کدِ تست، آنرا اجرا خواهید کرد. با این اجرا قطعا تست شما Fail میشود، چرا که هنوز سورس کدی که توسط آن تست شود، نوشته نشده است. اکنون باید سورس کدی که تحت تست قرار میگیرد را بنویسید. پس از این کار شما اجازه ندارید کد تست خود را تغییر دهید. لذا باید تا زمانیکه تست شما Pass شود، سورس کد خود را دیباگ کرده و تغییر دهید.
هر چرخه باید بسیار کوتاه باشد.
از مزایای TDD میتوان به موارد زیر اشاره کرد:
- دریافت سریعترین بازخورد
- ایجاد یک Living Specification دقیق
- Refactor کردن ایمنتر
- باگهای کمتر(ما آنها را قبل از استقرار پیدا میکنیم)
- اجبار در نوشتن کد به صورت SOLID
برای به دست آوردن همه این مزایا، باید اعضای تیم خود را به روش خوبی، به تست آلوده کنید. بنابراین همه باید تست بنویسند. آنها نیاز به آموزش مجدد دارند و شما باید فرهنگی را پرورش دهید که در آن کل تیم مسئول اتوماسیون تست و به طور کلی کیفیت تولیدات خود هستند. قبل از شروع به نوشتن تستِ قبل از اجرا، باید مطمئن شوید که تیمها و کد اپلیکیشن برای آن آماده است.
ابتدا باید تمرکز خود را به تستهای یونیت(یونیتهایی که ایزوله هستند)، و تستهای Component Integration ایزوله معطوف کنید. تستهای UI و Integration کند هستند، و به طراحی فشار نمیآورند. نوشتن و نگهداری آنها گران بوده و بسیار شکننده(تغییر پذیر) هستند. شما نمیخواهید یک هرم تست در اینجا قرار دهید.
و اما ۵ گام پیادهسازی TDD:
۱- گام اول: اعمال تغییرات اولیه در کد
یک کد قابل تست ایجاد کنید. یکی از بزرگترین چالشهای اجرای TDD، کد قدیمی با تعداد صفر Unit Test است. برای رفع این چالش، شما باید مانند تاتی تاتی یک کودک به جلو حرکت کنید. هیچ کس قرار نیست از صفر Unit Test به TDD با ۱۰۰ درصد Coverage پرش کند. با Refactoring و ایجاد کد قابل تست برای برنامههایی که ارزش آنها بیشتر است(ایجاد یک ماتریس اولویت دارد) آغاز کنید و ابتدا آنها را با Unit Testها پوشش دهید.
آنچه را که دارید دوباره مرور کنید و کد موجود را به کدی قابل تست و البته تست شده، ریفکتور کنید.
برای عیبیابی کد موجود:
- کد را تغییر دهید تا SOLID شود(اصول SOLID).
- همه کدهای غیر قابل تست(متدهای خصوصی، فراخوانی وابستگیهای خارجی) را در Wrapper Classها قرار دهید.
- از دست متدها و متغیرهای استاتیک خلاص شوید. کدی را که ماهیتاً در قالب Unit testing قابل تست نیست(مانند API های خارجی) را ایزوله نمایید.
- کد را به حالت Loosely-Coupled(همراهی آزادانه) منتقل کنید. به این فکر کنید که چگونه آنچه را که دارید به مجموعهای از قطعات تولید(Building Block) ساده تقسیم کنید تا هرکدام یک کار ساده انجام دهند.
- همه متدهای غیرناب(Impure Method) را به متدهای ناب(Pure Method)تبدیل کنید تا آنها از چیزی که برای محاسبه، به عنوان آرگومان در نظر گرفته نشده است، استفاده نکنند و همچنین چیزی که به آنها داده شده است را اصلاح ننمایند. چنین رویکردی باعث ایمنسازی میشود. با اطمینان میتوانید هزار بار آنرا فراخوانی نمایید، و مطمئن باشید به دلیل این فراخوانی، چیزی که انتظار آنرا ندارید تغییر نمیکند.
۲- گام دوم: برگزاری کارگاه آموزشی
کارگاههای آموزشی TDD و Unit Testing را سازماندهی کنید. شما باید یک استاد راهنمای TDD پیدا کرده، و برخی کارگاههای آموزشی در مورد آن و تست یونیت را برای تیم خود مهیا نمایید.
کارگاهها باید موضوعات اساسی زیر را پوشش دهند:
- Unit Testing چیست و چگونه با انواع دیگر تستها مقایسه میشود.
- الگوهایی برای نوشتن تستهای موثر
- Unit Testing Frameworkها
- تستهای ایزوله با استفاده از Stubها و Mockها
- Manual Mocking و Auto-Mocking
- Mocking Frameworkها
- Test Reportها
- Coverage Reportها
- TDD
کارگاهها باید موضوعات پیشرفته زیر را پوشش دهند:
- برخورد با Large Objectها و تست کردن کد به صورت Asynchronous
- سازماندهی تستها برای یک سیستم بزرگ
- Best Practiceهای Unit Testing
- TDD در کد Legacy
- قطع وابستگیها در کد Legacy
- استراتژیهای افزایش Unit Testing Coverage
- استفاده از تستهای Unit در Buildهای خودکار
شما همچنین می توانید چندین کارگاه آموزشی در زمینه توسعه رفتار محور(BDD) داشته باشید. این به شما کمک میکند تا چالشها را بدون اینکه بدانید چه مواردی را باید در تستهای خودکار پوشش دهید، برطرف نمایید. سپس میتوانیم الزامات پذیرش(معیارهای پذیرش یا Acceptance Criteria) را با استفاده از زبان خاص دامنه(DSL یا Domain Specific Language) برای پروژه بنویسیم، که پس از خودکارسازی، این سناریوها به عنوان اسناد زنده و قابل اجرا عمل میکنند. این نوع مستندات همیشه به روز هستند و هنگام کشف رفتار سیستم، منبعی برای حقیقت سیستم محسوب میشوند. شما با تستهای خودکار، فقط رفتارِ سیستم(برنامه تحت تست) را که از نظر کسب و کاری بیشترین اهمیت را دارد پوشش میدهید. عملگرا باشید و دنبال پوشش جادویی ۱۰۰ درصدی نباشید
۳- گام سوم: برنامهنویسی دو نفری
جلسات برنامهنویسی دو نفری(Pair-Programming) را با کسانی که TDD میدانند تشکیل دهید. شما باید کسانی را که هرگز در چنین محیطی کار نکردهاند با کسانی که در آنجا بودهاند جفت کنید. یعنی کسانیکه میدانند چگونه این کار را به درستی انجام دهند.
اگر بگویید “این سرعت ما را کاهش میدهد، ما نمیتوانیم وقت خود را صرف آن کنیم” ، پاسخ من این خواهد بود “این کندی یک توهم است.” تستنویسی به صورت Test First، به این معنیست که کیفیت کد بهتر خواهد شد، آنچنانکه پس از آن به دوبارهکاری و ریزش، احتیاج کمتری خواهد داشت و این یعنی نقص کمتر. در نهایت، نتیجه خواهد داد.
یکی از مزایای اصلی برنامهنویسی دو نفری، اشتراک دانش بین اعضای تیم است. اعضای آگاهتر باید نقش مربی را برای مبتدیان بازی کنند. به این ترتیب همه میتوانند در حالیکه تحت نظارت هستند، در طراحی، پیادهسازی و اجرای Unit Testها، ورود کنند.
۴- گام چهارم: ایجاداسپرینتهای بهینهسازی Unit Testing
اسپرینت مخصوصی را برای رهایی از بدهی فنی(Technical Debt) تنظیم نموده و پوشش Unit Testها را بهبود ببخشید تا همه بتوانند آنچه را که در کارگاهها آموختهاند تمرین کنند.
بدون تمرین، هر آنچه آموخته شده تبخیر خواهد شد. در طی آن اسپرینتهای خاص، توسعه دهندگان تمرین میکنند:
- نوشتن Unit Testها
- ریفکتور کردن کد برای Testableتر کردن آن
- Mock کردن انواع وابستگیها
- گنجاندن تستها در Buildهای خودکار
- استفاده از TDD برای توسعه کد جدید
۵- گام پنجم: ایجاد یک تیم ممتاز TDD در شرکت
یک تیم ممتاز TDD را در شرکت ایجاد کنید تا به تیمهای دیگر کمک کنند این روش را دنبال نمایند. این تیم به عنوان یک شرکت مشاور در داخل سازمان عمل میکند، که به همه کمک مینماید تا Best Practiceهای TDD را رعایت کنند. آنها این مشاوره را به همه تیمها ارائه میدهند.
در عرض چند ماه پس از نوشتن تعداد مناسب تست، شما میتوانید TDD را با چند تیم دنبال کنید. سپس میتوانید آن را در سازمان گسترش دهید و از این تیمها به عنوان یک الگو استفاده کنید. به یاد داشته باشید که این فرایند نباید اجبار شود، بلکه این امر باید به طور طبیعی با پرورش فرهنگ نوشتن Unit Testها، برای ویژگیهای جدید در اسپرینت انجام شود و بخشی از DoD(Definition of Done) شود. شما میتوانید دروازههای کیفیت را به منظور پوشش تست یونیت برای قابلیتهای افزوده شدهی جدید(به عنوان مثال، با استفاده از SonarQube) تنظیم نمایید تا پیشرفت در گسترش پوشش(Coverage) را تجسم کنید.
من اعتقاد دارم کیفیت نه از طریق بازرسی بلکه از طریق بهبود روند تولید حاصل میشود. وقتی TDD به درستی راهاندازی شود، میتواند مزایای بیشماری را به همراه داشته باشد و منجر به صرفهجویی در هزینه شود و یک ارزش واقعی برای کسب و کار فراهم نماید. وقتی TDD به درستی ستاپ نشده باشد یا بدون درک نحوه استفاده از آن ستاپ شده باشد، میتواند به شکل بدی منجر به اتلاف وقت و هزینه شود.