۱۳۸۸/۰۲/۱۷

آشنايي با mocking frameworks - قسمت دوم


استفاده از mocking frameworks :

تعدادي از چارچوب‌هاي تقليد نوشته شده براي دات نت فريم ورك مطابق ليست زير بوده و هدف از آن‌ها ايجاد ساده‌تر اشياء تقليد براي ما مي‌باشد:

Nmock : http://www.nmock.org
Moq : http://code.google.com/p/moq
Rhino Mocks : http://ayende.com/projects/rhino-mocks.aspx
TypeMock : http://www.typemock.com
EasyMock.Net : http://sourceforge.net/projects/easymocknet

در اين بين Rhino Mocks كه توسط يكي از اعضاي اصلي تيم NHibernate به وجود آمده است، در مجامع مرتبط بيشتر مورد توجه است. براي آشنايي بيشتر با آن مي‌توان به اين ويديوي رايگان آموزشي در مورد آن مراجعه نمود (حدود يك ساعت است).



خلاصه‌ا‌ي در مورد نحوه‌ي استفاده از Rhino Mocks :
پس از دريافت كتابخانه سورس باز Rhino Mocks ، ارجاعي را به اسمبلي Rhino.Mocks.dll آن، در پروژه آزمون واحد خود اضافه نمائيد.
يك Rhino mock test با ايجاد شيءايي از MockRepository شروع مي‌شود و كلا از سه قسمت تشكيل مي‌گردد:
الف) ايجاد شيء Mock يا Arrange . هدف از ايجاد شيء mock ، جايگزين كردن و يا تقليد يك شيء واقعي جهت مباحثي مانند ايزوله سازي آزمايشات، بالابردن سرعت آن‌ها و متكي به خود كردن اين آزمايشات مي‌باشد. همچنين در اين حالت نتايج false positive نيز كاهش مي‌يابند. منظور از نتايج false positive اين است كه آزمايش بايد با موفقيت به پايان برسد اما اينگونه نشده و علت آن بررسي سيستمي ديگر در خارج از مرزهاي سيستم فعلي است و مشكل از جاي ديگري نشات گرفته كه اساسا هدف از تست ما بررسي عملكرد آن سيستم نبوده است. كلا در اين موارد از mocking objects استفاده مي‌شود:
- دسترسي به شيء مورد نظر كند است مانند دسترسي به ديتابيس يا محاسبات بسيار طولاني
- شيء مورد نظر از call back استفاده مي‌كند
- شيء مورد آزمايش بايد به منابع خارجي دسترسي پيدا كند كه اكنون مهيا نيستند. براي مثال دسترسي به شبكه.
- شيءايي كه مي‌خواهيم آن‌را تست كنيم يا براي آن آزمايشات واحد تهيه نمائيم، هنوز كاملا توسعه نيافته و نيمه كاره است.
ب) تعريف رفتارهاي مورد نظر يا Act
ج) بررسي رفتارهاي تعريف شده يا Assert

مثال:
متد ساده زير را در نظر بگيريد:

public class ImageManagement
{
public string GetImageForTimeOfDay()
{
int currentHour = DateTime.Now.Hour;
return currentHour > 6 && currentHour < 21 ? "sun.jpg" : "moon.jpg";
}

}
آزمايش اين متد، وابسته است به زمان جاري سيستم.

using System;
using NUnit.Framework;

[TestFixture]
public class CMyTest
{
[Test]
public void DaytimeTest()
{
int currentHour = DateTime.Now.Hour;

if (currentHour > 6 && currentHour < 21)
{
const string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay();
Assert.AreEqual(expectedImagePath, path);
}
else
{
Assert.Ignore("تنها در طول روز قابل بررسي است");
}
}

[Test]
public void NighttimeTest()
{
int currentHour = DateTime.Now.Hour;

if (currentHour < 6 || currentHour > 21)
{
const string expectedImagePath = "moon.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay();
Assert.AreEqual(expectedImagePath, path);
}
else
{
Assert.Ignore("تنها در طول شب قابل بررسي است");
}
}

}
براي مثال اگر بخواهيم تصوير ماه را دريافت كنيم بايد تا ساعت 21 صبر كرد. همچنين بررسي اينكه چرا يكي از متدهاي آزمون واحد ما نيز با شكست مواجه شده است نيز نيازمند بررسي زمان جاري است و گاهي ممكن است با شكست مواجه شود و گاهي خير. در اين‌جا با استفاده از يك mock object ، اين وضعيت غيرقابل پيش بيني را با منطقي از پيش طراحي شده جايگزين كرده و آزمون خود را بر اساس آن انجام خواهيم داد.
براي اين‌كار بايد DateTime.Now.Hour را تقليد نموده و اينترفيسي را بر اساس آن طراحي نمائيم. سپس Rhino Mocks كار پياده سازي اين اينترفيس را انجام خواهد داد:

using NUnit.Framework;
using Rhino.Mocks;

namespace testWinForms87
{
public interface IDateTime
{
int GetHour();
}

public class ImageManagement
{
public string GetImageForTimeOfDay(IDateTime time)
{
int currentHour = time.GetHour();

return currentHour > 6 && currentHour < 21 ? "sun.jpg" : "moon.jpg";
}
}

[TestFixture]
public class CMocking
{
[Test]
public void DaytimeTest()
{
MockRepository mocks = new MockRepository();
IDateTime timeController = mocks.CreateMock<IDateTime>();

using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(15);
}

using (mocks.Playback())
{
const string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);
}
}

[Test]
public void NighttimeTest()
{
MockRepository mocks = new MockRepository();
IDateTime timeController = mocks.CreateMock<IDateTime>();
using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(1);
}

using (mocks.Playback())
{
const string expectedImagePath = "moon.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);
}
}
}

}
همانطور كه در ابتداي مطلب هم عنوان شد، mocking‌ از سه قسمت تشكيل مي‌شود:

MockRepository mocks = new MockRepository();
ابتدا شيء mocks را از MockRepository كتابخانه Rhino Mocks ايجاد مي‌كنيم تا بتوان از خواص و متدهاي آن استفاده كرد.
سپس اينترفيسي بايد به آن پاس شود تا انتظارات سيستم را بتوان در آن بر پا نمود:

IDateTime timeController = mocks.CreateMock<IDateTime>();
using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(15);

}
به عبارت ديگر در اينجا به سيستم مقلد خود خواهيم گفت: زمانيكه شيء ساعت را تقليد كردي، لطفا عدد 15 را برگردان.
به اين صورت آزمايش ما بر اساس وضعيت مشخصي از سيستم صورت مي‌گيرد و وابسته به ساعت جاري سيستم نخواهد بود.

همانطور كه ملاحظه مي‌كنيد، روش Test Driven Development بر روي نحوه‌ي برنامه نويسي ما و ايجاد كلاس‌ها و اينترفيس‌هاي اوليه نيز تاثير زيادي خواهد گذاشت. استفاده از اينترفيس‌ها يكي از اصول پايه‌اي برنامه نويسي شيءگرا است و در اينجا مقيد به ايجاد آن‌ها خواهيم شد.

پس از آن‌كه در قسمت mocks.Record ، انتظارات خود را ثبت كرديم، اكنون نوبت به وضعيت Playback مي‌رسد:
using (mocks.Playback())
{
string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);

}
در اينجا روش كار همانند ايجاد متدهاي آزمون واحد متداولي است كه تاكنون با آن‌ها آشنا شده‌ايم و تفاوتي ندارد.
با توجه به اينكه پس از تغيير طراحي متد GetImageForTimeOfDay ، اين متد اكنون از شيء IDateTime به عنوان ورودي استفاده مي‌كند، مي‌توان پياده سازي آن اينترفيس ‌را در آزمايشات واحد تقليد نمود و يا جايي كه قرار است در برنامه استفاده شود، مي‌تواند پياده سازي واقعي خود را داشته باشد و ديگر آزمايشات ما وابسته به آن نخواهد بود:

public class DateTimeController : IDateTime
{
public int GetHour()
{
return DateTime.Now.Hour;
}
}