‫Mocking با استفاده از Moq

Mocking روشی در Unit Test است که با کمک آن رفتار کلاس‌ها و آبجکت‌هایی که وابسته به منابع بیرونی بوده و غیر قابل کنترل هستند سنجیده می‌شود. منظور از آبجکت‌های غیر قابل کنترل، آبجکتی‌های مثل DateTime.Now در دات‌نت، کلاس‌های load اطلاعات از دیتابیس، کلاس‌های کار با deviceهای بیرونی مثل شبکه و فاکس و غیره می‌باشد. همان طور که می‌دانید این طور کلاس‌ها را نمی‌توان با کمک Unit Test مورد آزمایش قرار داد.

یکی از معروف‌ترین ابزارهایی که برای این کار وجود داشت Rhino Mocks بود. یک سال و خورده‌ای پیش سعی کردم از این کتابخانه در Unit Testهایم استفاده کنم. اما به دلیل آن که استفاده از این کتابخانه و فهم آن برایم سخت بود زیاد نتوانستم از آن استفاده کنم. البته نیاز به آن Unit Testها هم کم شده بود و نهایتاً mocking به فراموشی سپرده شد. طی چند روز گذشته که داشتم چند unit test جدید به کارویس اضافه می‌کردم باز هم نیاز به mocking پیدا کردم. نگران بودم که چطور باید با روش پیچیده Rhino Mocks کنار بیام. حدس زدم طی این یکی دو سال Rhino Mocks پیشرفت زیادی کرده و راحت‌تر شده یا این که اقلاً یا منابع یادگیری اون بیشتر شده یا شاید هم frameworkهای mocking جدیدی به وجود آمده‌اند.

با یک search در گوگل فهمیدم حدسم اشتباه نبوده است. کتابخانه جدیدی به اسم Moq وارد عرصه رقابت Mocking شده که خیلی راحت‌تر از Rhino Mocks است. Moq کاملاً بر اساس امکانات C# 3.5 نوشته شده و به همین علت می‌توان به راحتی با Lambda Expression با آن کار کرد. به مثالی که در همان صفحه معرفی Moq آمده دقت کنید تا متوجه سادگی آن شوید. البته Rhino Mocks هم امکان کار با Lambda Expression را دارد اما باز هم به نظر من Moq راحت‌تر و ساده‌تر است. برای مقایسه بین Moq و بقیه فریمورک‌ها به این نوشته اسکات هنزلمن مراجعه کنید. برای کسب اطلاعات بیشتر راجع به mocking و Rhino Mocks به این و این نوشته هم مراجعه کنید.

مشکل ارتقای اجزای نرم‌افزار

یکی از مشکلات بزرگی که نرم‌افزار نویس‌ها مدام با آن دسته و پنجه نرم می‌کنند مشکل ارتقا اجزا و بخش‌های مختلف یک نرم‌افزار است. برای این مشکل مثال‌های زیادی وجود دارند:
۱- برنامه را با VB6 نوشته‌اید ولی حالا که به VB.NET ارتقا داده‌اید متوجه شده‌اید که پیش‌فرض VB.NET برای اعضای کلاس private است نه public.
۲- برنامه را با NHibernate 1.2 نوشته‌اید ولی بعد از ارتقا به NHibernate 2.0 فهمیده‌اید که برنامه با آن configuration قدیمی کار نمی‌کند و باید web.config/app.config را هم عوض کنید.
۳- برنامه را با فلان library نوشته‌اید و بعد از ارتقا به نسخه جدید فهمیده‌اید که نسخه جدید به خیلی از مسائلی که قبلاً گیر نمی‌داد حالا گیر می‌دهد و نمی‌گذارد برنامه اجرا شود.
۴- در برنامه‌های ASP.NET از کنترل‌های Telerik استفاده کرده‌اید و بعد از ارتقا فهمیدید که tagهای استفاده شده در markup اسمشان عوض شده و حالا دچار parser error شده‌اید.
۵- در برنامه‌ای که برای ویندوز ۹۵ نوشته شده از یک API خاص استفاده شده که دیگر در ویندوزهای جدیدتر وجود ندارد.
۶- در بخشی از برنامه از نسخه x یک library استفاده کرده‌اید و بعداً لازم می‌شود بخشی از یک نرم‌افزار دیگر را به نرم‌افزار فعلی‌تان الحاق کنید اما این بخش جدید از نسخه y استفاده می‌کند نه x.
۷-…

این مسئله که برای برنامه‌نویسان عذاب‌آور و برای مدیران ترس‌آور است را می‌توان به دو بخش خارجی و داخلی تقسیم کرد. بخش خارجی مربوط است به dllها، libraryها، APIها و platformهایی که از خارج شرکت/تیم می‌آیند و معمولاً حق انتخاب زیادی در مورد آنها وجود ندارد. بخش داخلی مربوط است به اجزا و dllهایی که در داخل شرکت ساخته می‌شود و version زدن آنها یک مصیبت بزرگ است. در مورد dllهای داخلی مسئله backward comaptiblity و وابستگی به dllهای دیگر هم باید چک شود. چیزی که معمولاً در مورد dllهای خارجی بهتر رعایت می‌شود.

برای کاهش مشکلات ارتقا به نظر بنده موارد زیر را می‌توان در نظر گرفت:
۱- مثبت‌نگر باشید. ارتقا dllها خصوصاً در مورد dllهای خارجی معمولاً نوید دهنده امکانات بهتر و کامل‌تر است. معمولاً هدف از ارائه نسخه جدیدتر رفع اشکالات نسخ قبلی، بهبود عملکرد و… است. پس بنابراین خوشحال باشید که رفع مشکلات مربوط به نسخه جدید به معنی ارتقا کیفیت نرم‌افزار شما خواهد بود.
۲- پیروی از اصل KISS (Keep It Simple Stupid)‎: یعنی تا آنجا که می‌توانید برنامه را ساده نگه داشته و بیخودی از هر کتابخانه، class و dll دیگری استفاده نکنید.
۳- Dependency Injection: برنامه‌ها با حداقل وابستگی به هم نوشته شوند.
۴- موتوا قبل ان تموتوا (بمیرید قبل از آن که بمیرید): قبل از آن که مجبور شوید ارتقا دهید، ارتقا دهید. تولید کنندگان نرم‌افزار معمولاً قبل از ارائه نهایی نسخ جدید آنها را به صورت نسخه‌های آزمایشی ارائه می‌دهند و حتی قبل از آن کلی بحث راه می‌اندازند، نظر سنجی و اطلاع‌رسانی می‌کنند که ما قرار است فلان چیز را به نرم‌افزارمان اضافه کنیم یا تغییر بدهیم. بنابراین فرصت خیلی زیادی دارید که خود را به موقع ارتقا دهید.
۵- پیروی از تجارب TDD مثل Unit Test و Continuous integration: این راه‌حل خصوصاً در مورد dllهای داخلی خیلی خوب جواب می‌دهد. اگر برای همه بخش‌هایی ممکن unit test نوشته باشید آن وقت نگرانی کمتری برای ارتقا دارید چون خیلی سریع می‌فهیمد که آیا چیزی خراب شده یا نه. Continuous integration هم به همین شکل کمک می‌کند که در حداقل زمان ممکن خطا را کشف کنید.
۶- به هیچ وجه Warningهای هنگام کامپایل را دستکم نگیرید. خیلی از warningهای نسخه امروز به compile errorهای نسخه فردا تبدیل خواهند شد.
۷- استفاده از الگوهای جدیدی مثل MVC چون کار تست را راحت‌تر کرده و decoupling که با استفاده از dependency injection محقق می‌شود را بهتر اجرا می‌کنند.

شما چه راه‌هایی برای کاهش این طور مشکلات سراغ دارید؟

Dependency Injection

همیشه وقتی سورس نرم‌افزارهای Open Source را بررسی می‌کردم به یک چیز بی‌معنی برمی‌خوردم: استفاده بی‌مورد از interface. مثلاً کلاسی را پیدا می‌کردم که پیاده‌سازی یک اینترفیس خاص بود در حالی که خود آن اینترفیس هم فقط در همان یک جا مورد استفاده قرار گرفته بود. تا جایی که من می‌دانستم استفاده از interface وقتی خوب است که بخواهیم چندین و چند کلاس داشته باشیم که بخواهیم آن یک interface خاص را پیاده‌سازی کرده باشند. چند وقت پیش بالاخره به صرافت افتادم و در StackOverflow پرسیدم که چرا این روزها استفاده از interface این قدر باب شده است.

جواب خیلی واضح و قابل قبول بود. چون با استفاده از interfaceها می‌توان مفاهیمی مثل Dependency Injection و Decoupling را در نرم‌افزار پیاده‌سازی کرد. استفاده از این مفاهیم یعنی کلاس‌ها و دیگر بخش‌های برنامه را طوری بنویسیم که در حد ممکن از دیگر بخش‌ها بی‌خبر بوده و در نتیجه به آن وابستگی نداشته باشند. برنامه‌هایی که به این روش نوشته می‌شوند مدیریت و نگهداری راحت‌تری دارند و خواناتر هستند. علاوه بر این‌ها تکنیک‌های جدیدی که در TDD و Mocking استفاده می‌شوند در برنامه‌هایی که به روش Dependency Injection نوشته شده‌اند خیلی کاراتر و راحت‌تر عمل می‌کنند.

‫Mocking و Rhino Mocks

از همان اولین باری که اسم Mock و Mocking را شنیدم حس کردم چیز گنگی است و تا لازم نشده سراغ آن نروم. اما به تازگی فهمیدم که اولاً با Mocking بعضی unit testهای غیر ممکن، ممکن می‌شوند، ثانیاً استفاده از آن می‌تواند تست نرم‌افزار را خیلی راحت‌تر و شیرین‌تر کند. قاعدتاً همه اسم Mocking را شنیده‌اند و تا حدودی می‌دانند به چه در می‌خورد و حتی شاید مثال‌هایی را از آن دیده باشند. اگر واقعاً چیزی از آن نمی‌دانید به این مطلب و این مطلب از وحید مراجعه کنید.

مشکل من با Mocking از آنجا بود که نمی‌دانستم از کجا باید شروع کنم و چه چیزی را باید یاد بگیرم. من تمام وبلاگ/سایت نویسنده Rhino Mocks که تبدیل به استانداردی در دنیای Mocking شده را زیر و رو کردم، همینطور StackOverflow و گوگل را. اما ظاهراً خیلی از مطالبی که پیدا می‌کردم از جمله مطالب خود وبلاگ/سایت نویسنده آن یعنی Ayende Rahien از تاریخ گذشته و اصطلاحاً depricate شده بودند یا این که فهمیدن آنها خیلی سخت بود. هر کسی از هر روشی که خوشش آمده بود استفاده کرده بود و هیچ منبع درست و حسابی پیدا نمی‌شد. خوشبختانه با همه بالا و پایین‌ها متوجه شدم که دو روش کلی برای انجام Mocking با Rhino Mocks وجود دارد:

۱- روش کلاسیک یا Record-Replay: این روش قدیمی‌تر است و مبتنی بر انجام یک سری عملیات پشت سر هم، ضبط و سپس تکرار آنهاست. بیشتر منابعی که در اینترنت پیدا کرده بودم از این روش استفاده می‌کردند.

۲- روش AAA: این روش بعد از ورود lambda expressionها و extension methodها به C#‎ ابداع و مورد استفاده قرار گرفته است. بیشتر افراد معتقد هستند این روش خیلی راحت‌تر و خواناتر است. من هم این روش را برای ادامه کارم انتخاب کرده‌ام. یک مطلب بسیار خوب در مورد یادگیری این روش در اینجا آمده است.

برای استفاده از Mocking در تست‌ها باید آماده برداشتن یک قدم مهم بود: استفاده از مفاهیم Decoupling و Dependency Injection در سطح کد یا حداقل استفاده از کلاس‌های Virtual و غیر Static. چون Rhino Mock و بقیه ابزارها یا فقط از interfaceها یا از کلاس‌هایی که متودشان به صورت virtual تعریف شده باشد می‌توانند استفاده کنند. دلیل آن هم اصلاً غیر منطقی نیست. به یاد داشته باشید برای استفاده از Lazy Loading در NHibernate هم باید setterها و getterها به صورت Virtual تعریف می‌شد.

چند منبع:
۱-  مطلب اول و دوم وحید
۲- صفحه‌ی رسمی Rhino Mocks
۳- وبلاگ پدیدآورنده‌ی Rhino Mocks
۴- نوشته‌ی مارتین فولر درباره فرق Stub و Mock
۵- راهنمای خیلی خوبی برای شروع Mocking با استفاده از روش AAA
۶- صفحه‌ی ویکی‌پدیا درباره‌ی Mocking
۷- توضیحات پدیدآورنده‌ی Rhino Mocks درباره روش AAA

‫کمی درباره‌ی Mock و Stub

هم Mock و هم Stub در unit testهایی استفاده می‌شوند که شخص نمی‌تواند یا نمی‌خواهد از بعضی objectهای مورد نیاز استفاده نماید. مثلاً قرار است متود ارسال ایمیل به امور فروش در صورت کاهش موجودی برخی کالاها تست شود. فرض کنید موقع تست امکان ارسال ایمیل به علت قطعی اینترنت وجود ندارد. در این حالت آبجکت مربوط به ارسال ایمیل به نوعی شبیه‌سازی می‌شود. این کار باعث می‌شود تست مورد نظر روی خود عملیات کاهش موجودی و ارسال ایمیل تمرکز کند و کاری به مشکلات مربوط به ارسال ایمیل نداشته باشد. به غیر از ارسال ایمیل حالات دیگری هم وجود دارند که در آن استفاده از object مورد نظر امکان‌پذیر نیست. مثلاً خواندن اطلاعات از یک device وقتی که خود device را نداریم، محاسبه حقوق بر اساس اطلاعات کارمندان یک شرکت وقتی که اطلاعات هیچ کدام از کارمندان را نداریم و بسیاری حالات دیگر. خود شبیه‌سازی هم صرفاً محدود به Mock و Stub نمی‌شود. مثلاً می‌توان از الگوهایی مثل Object Mother هم استفاده کرد.

Mock با استفاده از برنامه‌های کمکی که به اسم Isolation Framework یا Mocking Framework معروف هستند انجام می‌شود. Rhino Mocks معروف‌ترین و پراستفاده‌ترین Mock Framework در دات‌نت است. در روش Mock ما آن آبجکتی را که قرار است شبیه‌سازی شود را معرفی می‌کنیم و سپس انتظاراتمان را از آن بیان می‌کنیم. مثلاً فرض کنید آبجکتی به اسم FaxReceiver دارید که یکی از متودهایش وظیفه دارد به شما بگوید از تاریخ آخرین عوض کردن کاغذ دستگاه فاکس، چند عدد فاکس دریافت کرده است. متود مورد تست شما از این آبجکت استفاده می‌کند و هر وقت که تعداد فاکس‌های دریافتی بیش از هزار عدد شد به اپراتور دستگاه پیغام دهد که موقع عوض کردن کاغذ دستگاه فرا رسیده است. با روش‌های معمولی تست نمی‌توان این متود را تست کرد مگر این که هزار عدد فاکس دریافت کنید. در اینجاست که از روش‌های شبیه‌سازی مثل Mock استفاده می‌شود. برای این کار آبجکت FaxReceiver به Mock Framework معرفی شده و به او می‌گوییم هر وقت متود تعداد فاکس‌های دریافتی فراخوانی شد از او «انتظار» داریم مقدار هزار را برگرداند.

مثال بالا را می‌توان با Stub هم انجام داد. با این تفاوت که در مورد Stub هیچ Framework یا Library وجود ندارد و شما مجبور هستید همه یا بخشی از آبجکت/متود مورد نظر را خودتان بنویسید. مثلا متودی بنویسید که به جای مراجعه واقعی به دستگاه فاکس و خواندن تعداد فاکس‌های دریافتی از آن، همیشه عدد هزار را برگرداند. با این که کار با Stub خیلی سخت‌تر از کار با Mock است ولی بعضی حالات وجود دارند که فقط با Stub می‌شود آنها را پیاده‌سازی کرد. مثلاً وقتی که نگهداشتن State برای ما مهم باشد. به عنوان مثال فرض کنید در تست دیگری بخواهیم بدانیم چند بار متود فراخوانی تعداد فاکس‌های دریافتی فراخوانی شده است. انجام این کار با Mocking امکان‌پذیر نیست و باید از روش Stub استفاده شود.

منابع:
Mocks Aren’t Stubs
Mock object
آشنايي با mocking frameworks (چارچوب‌هاي تقليد) – قسمت اول
آشنايي با mocking frameworks – قسمت دوم