در نوشته‌های قبلی دیدیم که چطور با استفاده از stub ها کد کلاسی وابسته زیر تست را mock کردیم. نوشته انگلیسی روز چهاردهم را در این آدرس می‌توانید مطالعه کنید.

یک روز دیگر، یک تست دیگر

برای این نوشته نیز مثال فروشگاه مطالب قبلی را ادامه می‌دهیم. تست بعدی زمانی است که کاربر یک آیتم را سفارش می‌دهد و تعداد سفارش صفر است و InvalidOrderException باید throw شود.

در ظاهر این یک تست ساده به نظر می‌رسد: هر سفارش لیستی از آیتم‌ها دارد اگر تعداد هر یک از آیتم‌ها برابر صفر بود یک exception را throw‌ کن. در اغلب موارد انتظار داریم همراه exception یک پیغام هم برگردد که مشخص کند مشکل کجا بوده است، اما حالا برای ساده نگه داشتن مثال این best practice را نادیده می‌گیریم (در نوشته بعدی به آن خواهیم رسید) به نظر ساده می‌آید، این طور نیست؟ اجازه بدهید تست را بنویسیم

کد بالا ساده‌ترین حالت ممکن است. اگر سری نوشته‌های ۳۰ روز با TDD را از ابتدا دنبال کرده‌اید، چیز جدیدی اینجا نیست. تست خود را با ویژگی Test و همچنین ExpectedExpection معرفی می‌کنیم که به NUnit می‌گوید که انتظار یک exception از نوع InvalidOrderException داشته باشد. تعدادی کامنت هم قرار دادیم که یادآور الگوی AAA باشد و بخش‌های Arrange و Act‌ و Assert را مشخص کند. اغلب توسعه‌دهندگان TDD از بخش Arrange شروع می‌کنند که مشکلی هم برای شروع از این قسمت نیست. شخصاً تمایل دارم از بخش Assert شروع کرده و به عقب بروم. من دوست دارم ابتدا assert را بنویسم که می‌توانم از کلاس تست مشتقش کنم. با داشتن assert از ابتدا می‌دانم که چه چیزی را در بخش Act باید فراخوانی کنم تا نتیجه مورد انتظار تست را داشته باشم و با نوشتن Act در مرحله دوم می‌توانم مطمئن شوم که در قسمت Arrange تنها متغیرها، mock و stub هایی که برای تست در بخش Act نیاز است را تنظیم می‌کنم.

برای این تست یک stub برای OrderDataService نیاز دارم. از آنجایی که انتظار دارم در این تست یک exception ایجاد شود و نه اینکه یک سفارش ثبت شود پس توقع دارم که متد Save اصلاً صدا زده نشود (چون در صورت فراخوانی دردسر بزرگی خواهم داشت!)

تا اینجا همه چیز خوب است، حالا باید قسمت Act را بنویسیم

در مثال قبلی، من مقدار متد PlaceOrder را گرفتم و در بخش Assert استفاده کردم. در این مثال یک exception انتظار دارم، پس نتیجه بیهوده است. حالا نوبت به قسمت Arrange می‌رسد. با نگاه به کد اولین چیزی که لازم دارم مشخص می‌شود یک وهله (instance) از OrderService است. تست دیگرم نیز از OrderService استفاده می‌کنم. من مایلم تست‌هایم را تا حد ممکن DRY یا Don’t Repeat Yourself نگه دارم و دلیلی نیست که گام‌های ایجاد این instance مانند مطلب قبلی به یک setup method منتقل نشود

به نیمه راه رسیدیم. اگر به یاد داشته باشید OrderService یک وابستگی به OrderDataService دارد که باید برایش یک mock ایجاد کنم. خوشبختانه می‌توانم mock را به عنوان یک متغیر تعریف کرده و به سازنده (constructor) مربوط به OrderService در TestFixtureSetup تزریق کنم.

وقتی این کار انجام شد می‌توانم کد موجود را برای استفاده از این متغیرها به جای local instance ها refactor کنم.

همانطور که مشاهده می‌کنید خطوط مربوط به ایجاد mock های OrderDataService و OrderService ‌را حذف کردم. اما بعد از refactor برای استفاده از متغیرهای جدید کد من خطای کامپایلی دریافت می‌کند.

ما به InvalidOrderException تا حالا نیاز نداشتیم. اما حالا که تستی داریم که exception ای از این نوع ایجاد می‌کند باید یکی بسازیم.

من دوست دارم که پروژه را به شدت مرتب نگه دارم. به عنوان بخشی از این نگاه، exception های سفارشی را در پوشه مخصوصی در پروژه نگه می‌دارم که باعث می‌شود در فضا نام (namespace) مربوط به خود هم قرار بگیرند. در پروژه TddStore.Core پوشه‌ای به نام Exceptions ایجاد می‌کنم. یک کلاس جدید به نام InvalidOrderException در این پوشه می‌سازم و فعلاً پیاده‌سازی‌ام برای این کلاس به سادگی کد زیر خواهد بود:

تنها لازم است از using برای اشاره به فضا نام TddStore.Core.Exceptions استفاده کنم. بعد از انجام این کار تست اصلی پاس می‌شود و بنابراین می‌توانم به تست جدید برگردم. اول کمی refactor برای کدهای موجود نیاز داریم سپس لازم است روی بخش Arrange کار کنیم. ابتدا باید متغیرها را برای فراخوانی PlaceOrder تنظیم کنیم پس من customerId و shoppingCart را تعریف می‌کنم. در حالی که روی این موضوع کار می‌کنم آیتمی را به shoppingCart با مقدار صفر اضافه می‌کنم

سپس لازم است تا mock را آماده کنم

بر خلاف تست قبلی می‌خواهم مطمئن شوم که متد Save در OrderDataService فراخوانی نمی‌شود. همانطور که در نوشته قبلی این سری نوشته‌ها دیدید Just Mock‌ (محصولی از شرکت Telerik) امکان اجرای یک بار stub ای که exception ایجاد می‌کند را می‌دهد. بنابراین به صورت مشابه از متد OccursNever استفاده می‌کنم که بدان معنی است که این متد در حوزه تست فراخوانی نشود.

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

با اجرای تست‌ها می‌بینم که هر دو تست Pass می‌شوند.

کار من تمام شد!

آیا کار تمام شده است؟

در نوشته بعدی خواهیم دید که یک اشکال در تست‌ها به ما حس اطمینان اشتباهی می‌دهد.

ادامه دارد…