۳۰ روز با TDD: روز دوازدهم - کار با Stub ها

روز دوازدهم از مجموعه نوشته‌های ۳۰ روز با توسعه آزمون محور

نوشته به زبان انگلیسی روز دوازدهم را در این آدرس می‌توانید مطالعه کنید. در نوشته قبلی این سری یک پروتوتایپ از اینکه PlaceOrder برای یک OrderService در یک برنامه e-commerce چطور باید باشد به شما نشان دادم. برای نمایش mocking ما یک نسخه کاربردی از این منطق تجاری (business logic) را با TDD پیاده‌سازی خواهیم کرد. این به آن معنی است که با یک نیازمندی تجاری شروع می‌کنیم:

تصور کنید که کاربر به برنامه وارد شده (login کرده است) و آیتم‌هایی را در سبد خرید قرار داده است. برنامه باید این قابلیت را به کاربر بدهد که بر اساس آیتم‌های داخل سبد خرید یک سفارش را ثبت کند. کاربران باید بتوانند هر تعداد دلخواه از آیتم‌ها را سفارش دهند. کاربران نباید تعداد صفر یا کمتر از صفر هر یک از آیتم‌ها را سفارش دهند. اگر کاربری تعداد صفر یا کمتر از صفر را برای محصولی انتحاب کند برنامه باید یک استثناء (Exception) ایجاد کند و کل سفارش باید لغو شود (بدون اینکه سبد خرید خالی شود) به محض اینکه اعتبارسنجی تعداد کالاها انجام شد، سفارش باید در دیتابیس از طریق سرویس مربوطه ذخیره شده و مشتری هم به آن مرتبط شود. فراخوانی‌های سیستم فاکتور و ارسال سفارش بر اساس گردش کار ‌آن‌ها باید انجام شود. یک رکورد لاگ باید ایجاد شود که مشخص کند سفارش ثبت شده است. اگر سفارش به هر دلیلی غیر از اعتبارسنجی تعداد کالای سفارش داده شده ثبت نشود باید خطا لاگ شده و exception ایجاد شود. بعد از ثبت موفق سفارش، سبد خرید خالی شده و شناسه سفارش (order id) باید از دیتابیس برگردانده شود.

این یک نیازمندی تجاری (business requirement) طولانی است. طبیعتاً باعث می‌شود که چندین unit test بنویسیم ولی اجازه بدهید با ساده‌ترینشان شروع کنیم:

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

این یک مورد ساده و در عین حال یک شروع خوب است. چون این اولین نیازمندی و اولین تست است، من هیچ کد یا حتی solution ای در Visual Studio ندارم. برای شروع من یک Solution خالی در Visual Studio برای پروژه‌ام ایجاد می‌کنم که TddStore نام دارد. سپس یک پروژه به نام TddStore.UnitTests برای تست‌ها ایجاد می‌کنیم و از Nuget برای ایجاد رفرنس به NUnit در پروژه TddStore.UnitTests استفاده می‌کنم. نحوه رفرنس دادن را در روز سوم یاد گرفتیم. بعد از تکمیل این مراحل Solution من اینطور باید به نظر برسد

کلاس Class1 را به OrderServiceTests تغییر نام می‌دهیم. از خواص (attribute) مربوط به NUnit برای نوشتن اولین تست استفاده می‌کنیم

در این مثال فرض می‌کنیم تیم دیگری نیز بر روی برنامه در حال کار است و پیاده‌سازی ShoppingCart و Order و همچنین Interface های OrderDataService و BillingService و FulfillmentService  و LoggingService را در یک پروژه class library انجام داده است. این پروژه که TddStore.Core نام دارد را با استفاده از این فایل‌ها می‌توانید ایجاد کنید. بعد از ایجاد پروژه TddStore.Core باید رفرنس آن را به پروژه TddStore.UnitTests اضافه کنید.

برای این تست من نیاز دارم که با فراخوانی متدی که باید پیاده‌سازی شود از کلاسی که باید پیاده‌سازی شود یک سفارش ثبت کنم. مطابق معمول گردش کار TDD ابتدا تست را می‌نویسیم:

فهمیدن کد این تست باید خیلی ساده باشد. در قسمت Arrange من یک وهله (instance) از سبد خرید ایجاد می‌کنم و به آن یک آیتم اضافه می‌کنم. همچنین یک سفارش و شناسه مشتری فرضی برای تستم ایجاد می‌کنم. در نهایت یک OrderService برای تست ایجاد می‌کنم.

دو بخش Act و Assert هم نیازی به توضیح خاصی ندارند و مراحل ثبت سفارش و دریافت شناسه مورد آزمون قرار می‌گیرند. در این مرحله من تست را اجرا می‌کنم تا fail شدنش را ببینم و سپس شروع به نوشتن ساده‌ترین کد ممکن برای pass شدن تست می‌کنم. در نهایت به این کد می‌رسم:

بر اساس شرایط من باید رفرنس به OrderDataService ایجاد کنم. کد را refactor کرده و از طریق تزریق وابستگی با سازنده آن را تغییر می‌دهم

من به متد PlaceOrder دسترسی به instance ای از کلاس OrderDataService (از طریق اینترفیس IOrderDataService) می‌دهم ولی الان تست من دیگر کامپایل نمی‌شود. دلیل آن این است که سازنده (Constructor) پیش فرض OrderService دیگر وجود ندارد، نیاز است تا چیزی که IOrderDataService را پیاده‌سازی کرده به OrderService پاس بدهم

قدم بعدی

همان‌طور که در نوشته‌های قبلی اشاره شد از JustMock شرکت Telerik استفاده می‌کنیم. می‌توانید نسخه Lite آن را از طریق Nuget به پروژه اضافه کنید.

حالا که به JustMock دسترسی داریم، یک stub برای IOrderDataService ایجاد می‌کنیم:

اولین مرحله ایجاد شی mock است. قبل از این کار باید با استفاده از using فضانام Telerik.JustMock را به کلاس اضافه کنید. در خط ۲۱ من یک شی mock شده از اینترفیس IOrderDataService ایجاد کردم. چون این شی IOrderDataService را پیاده‌سازی می‌کند می‌توانم به عنوان آرگومان سازنده آن را به OrderService پاس بدهم همانطور که در خط ۲۲ می‌بینید. در ادامه کمی کد به متد PlaceOrder در کلاس OrderService اضافه می‌کنیم تا از IOrderDataService استفاده کند.

بر اساس تست کیس ما تنها نیاز داریم که به متد تست یک سبد خرید با حداقل یک آیتم پاس بدهیم و یک شناسه سفارش تحویل بگیریم. در این تست کیس هیچ توضیحی درباره اینکه اگر اعتبارسنجی به مشکل بخورد یا اینکه چطور بر اساس سبد خرید یک سفارش بسازیم وجود ندارد. unit test جاری پاسخگوی تست کیس تعریف شده است.

ما هنوز (و البته همیشه) در فازی هستیم که ساده‌ترین کد ممکن برای پاس شدن تست‌ها را بنویسیم. در این مرحله شما احتمالاً چند سوال خواهید داشت. اعتبارسنجی چطور انجام می‌شود؟ چطور مطمئن شویم که وهله (instance) سفارشی که ساخته شده درست ساخته شده است؟ جواب هر دو این سوالات این است که برای انجام نیازمندی‌های جدید باید تست بیشتری بنوسیم. وقتی تست‌ها را نوشتیم حالا می‌توانیم کد بنویسیم. این مساله را در نوشته‌های بعدی این سری خواهیم دید.

در این مرحله تست‌ها را دوباره اجرا می‌کنیم. تست باید fail شود چرا که با وجود اینکه همه وابستگی‌های مورد نیاز را تامین کردم، هنوز شناسه سفارش مورد انتظار را دریافت نمی‌کنم.

گرچه یک شی mock شده برای OrderDataService ایجاد کردیم اما هنوز نگفتیم وقتی فراخوانی شد چه کار باید انجام دهد. با اشاره به لیست انواع mock ها که در نوشته قبلی توضیح داده شدند، آنچه الان داریم یک Dummy است. ما باید آن را به یک Stub واقعی ارتقاء دهیم.

در خط ۱۴ من از دستور Mock.Arrange برای setup کردن شی mock شده orderDataService استفاده می‌کنم که در واقع این شی را به یک stub تبدیل می‌کند. متد Arrange یک عبارت Linq می‌گیرد که مشخص می‌کند برای کدام متد می‌خواهم رفتار تعریف کنم. در این مثال من به stub می‌گویم که به فراخوانی متد Save پاسخ دهد. به عنوان بخشی از این عبارت Linq می‌توانم یک لیست پارامتر برای stub تعریف کنم. می‌توانم یک مقدار مشخص را تعیین کنم. به عنوان مثال، اگر متد Save یک int بگیرد، می‌توانم مشخص کنم فقط زمانی که مقدار ۴۲ پاس داده می‌شود پاسخ بررسی شود. اگر mock را با مقداری غیر ز ۴۲ فراخوانی کنم مقدار پیش فرض آن نوع داده بازگشتی (مثلاً صفر برای int) را برمی‌گرداند. به این دلیل بود که وقتی قبلاً تست‌ها را اجرا کردم شی mock شده orderDataService یک guid خالی (همه صفر) برگرداند. این موضوع که loose mocking نیز نامیده می‌شود در نوشته‌های بعدی مورد بررسی قرار خواهد گرفت.

در این مثال من یک وهله (instance) از شی Order را پاس می‌دهم و از Matcher استفاده می‌کنیم. Matcher در واقع روشی است که به یک arrangement بگوییم نگران مشخصات پارامترها نباشد. من فقط می‌خواهم یک رفتار برای پارامترها تعریف کنم که از یک الگوی خاص پیروی می‌کنند. در این مثال به JustMock می‌گویم که فقط پاس داده شن شی Order برایم مهم است. برایم مهم نیست از کجا می‌آید یا چه چیزی داخلش است. Matcher ها ابزار قدرتمندی در mocking هستند که در نوشته‌های بعدی به آن اشاره خواهم کرد. حالا اگر تست را دوباره اجرا کنم pass می‌شود.

ادامه دارد…