۳۰ روز با TDD: روز دهم - بررسی بیشتر Refactoring‌ و NUnit

در نوشته روز نهم، درباره Refactoring صحبت کردیم، نوشته به زبان انگلیسی روز دهم را در این آدرس می‌توانید مطالعه کنید. در روز دهم با امکانات بیشتری در NUnit و همچنین Refactoring آشنا خواهیم شد.

بهترین شمشیرها از هر دو طرف می‌برند

نه تنها مهم است که به صورت دوره‌ای کد business logic خودمان را refactor کنیم، بلکه لازم است به صورت دوره‌ای تست‌ها را نیز بررسی کنیم. به یاد داشته باشید که تست‌ها هم نوعی کد هستند و نیازمند نگهداری مشابه. اجازه بدهید نگاهی به تست‌های جاری‌مان داشته باشیم:

یک نگاه سریع به این کد، یکی از اهداف مشترک refactoring را آشکار می‌کند: تکرار کد. اگر به خط‌های ۱۶ و ۲۹ و ۴۲ نگاه کنید می‌بینید که من مرتباً وهله‌های (instance) جدیدی از کلاس StringUtils برای هر تست ایجاد کردم. در کل من مایل به رعایت اصل DRY هستم. DRY سرنام عبارت Don't Repeat Yourself است. در این مثال من مشخصاً و زمانی که نیازی ندارم در حال تکرار کدم که وهله‌ای از StringUtils ایجاد می‌کند هستم. خوشبختانه NUnit مکانیزم برای کمک به ما برای DRY شدن کدمان فراهم می‌کند.

اما پیش از اینکه به سراغ تغییر تست‌ها برویم، لازم است سوالی را از خودمان بپرسیم: **از کجا باید بدانم که تغییر دادن تست‌ها باعث شکستن آن‌ها نمی‌شود؟**وقتی ما کد business logic برنامه را تغییر می‌دادیم، آزمون‌های واحد (unit test) داشتیم که به ما اطمینان می‌داد نتایج refactoring ما همچنان نیازهای تجاری که بر اساس آزمون‌های واحد تعریف شده‌اند را پوشش می‌دهند. تا زمانی که تست‌ها ما پاس می‌شدند، مطمئن بودیم که business logic همچنان نیازمندی‌های ما را برآورده می‌کند.
اگر واقعاً TDD را تمرین کرده باشیم، یعنی فقط بر اساس تست‌هایمان کد نوشته باشیم (و اول هم تست نوشته باشیم) آنگاه می‌توانیم از business logic برای اعتبارسنجی تست‌هایمان استفاده کنیم. این همان شمشیر دو لبه است و بدان معنی است که می‌توانیم تست‌هایمان refactor کنیم و تا زمانی که تست‌ها همچنان پاس می‌شوند و با شرط آنکه در همین حین business logic را تغییر نداده باشیم، تست‌های ما همچنان معتبر هستند.

در خصوص refactor تست‌ها، کاری که می‌خواهم انجام بدهم این است که کدی بنویسم که وهله‌ای از StringUtils برای استفاده در همه تست‌ها را ایجاد کند. تاثیر واقعی این کار وقتی در نوشته‌های بعدی درباره mocking صحبت کنیم نشان داده خواهد شد. معمولاً این کار را با ایجاد یک متد قابل استفاده مجدد انجام می‌دهم یا در این مورد خاص با استفاده از DI Framework اما NUnit راه بهتری را ممکن می‌کند.

میان ویژگی‌های مختلف NUnit قابلیت تعریف متدهای SetUp وجود دارد. یک متد setup در NUnit با استفاده از خاصیت SetUp به شکل زیر تعریف می‌شود. وقتی یک متد SetUp در یک test fixture در NUnit تعریف می‌شود قبل از اجرای هر تست در آن کلاس test fixture اجرا خواهد شد:

من یک متغیر وهله خصوصی (private instance) از نوع StringUtils تعریف کردم و متدی به نام SetupStringUtilsTests ایجاد کردم که در آن وهله جدیدی از کلاس StringUtils‌ را به متغیر خصوصی مربوطه انتساب می‌دهم. قدم بعدی refactor کردن تست‌ها برای استفاده از این متغیر است:

اجرای تست‌ها نشان می‌دهد که آن‌ها بعد از این تغییر همچنان پاس می‌شوند

این کار قطعاً تست‌های ما را خواناتر کرده است. همچنین به ما این قابلیت را داده تا کدی که از StringUtils وهله می‌سازد را یک جا کپسوله‌سازی کنیم. در واقع اگر کد ما تغییری کند، ما فقط یک متد را باید تغییر بدهیم نه n تا تست! استفاده از خاصیت SetUp به ما این قابلیت را می‌دهد تا متدهایی را تعریف کنیم که قبل از هر تست اجرا شوند. در شرایطی که کلاس زیر تست ما وضعیتی دارد که هر بار باید قبل از اجرای هر تست مجدداً ایجاد شود، استفاده از SetUp گزینه خوبی است. اما StringUtils ما چنین وضعیتی ندارد و نیازی نیست هر دفعه دوباره از اول ایجاد شود. اینجاست که از می‌توانیم از خاصیت TestFixtureSetUpبرای setup method خودمان استفاده کنیم. این خاصیت مشابه SetUp عمل می‌کند، اما به جای اجرا پیش از هر تست، یک بار برای هر instance از کلاس test fixture اجرا می‌شود. در مثال ما کلاس test fixture‌ سه تست دارد. در حالی که SetUp باعث اجرای سه مرتبه‌ای وهله‌سازی StringUtils می‌شود، TestFixtureSetUp یک بار برای هر سه تست ما اجرا و از یک وهله مشترک از کلاس StringUtils که توسط setup method ایجاد شده استفاده می‌کند.

از آنجایی که ما هر دفعه به یک وهله جدید از StringUtils نیاز نداریم، کد را برای استفاده از TestFixtureSetUp به شکل زیر تغییر می‌دهیم:

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

آخرین نکته درباره SetUp و TestFixtureSetUp‌ این است که NUnit فقط یک وهله از این خاصیت‌ها را در هر test fixture پشتیبانی می‌کند. شما می‌توانید از هر کدام یکی در test fixture خود داشته باشید اما مثلاً امکان مزین کردن دو متد با SetUp وجود ندارد. این کار موقع کامپایل خطایی به شما نمی‌دهد، اما در صورت تخطی، NUnit نمی‌تواند تست‌های شما را اجرا کند.

ادامه دارد…