۱۳۸۸/۰۷/۱۸

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


آزمون واحد كلاس نگاشت تهيه شده

در مورد آشنايي با آزمون‌هاي واحد لطفا به برچسب مربوطه در سمت راست سايت مراجعه بفرمائيد. همچنين در مورد اينكه چرا به اين نوع API كلمه Fluent اطلاق مي‌شود، مي‌توان به تعريف آن جهت مطالعه بيشتر مراجعه نمود.

در اين قسمت قصد داريم براي بررسي وضعيت كلاس نگاشت تهيه شده يك آزمون واحد تهيه كنيم. براي اين منظور ارجاعي را به اسمبلي nunit.framework.dll به پروژه UnitTests كه در ابتداي كار به solution جاري در VS.Net افزوده بوديم، اضافه نمائيد (همچنين ارجاع‌هايي به اسمبلي‌هاي پروژه NHSample1 ، FluentNHibernate ، System.Data.SQLite ، NHibernate.ByteCode.Castle و Nhibernate نيز نياز هستند). تمام اسمبلي‌هاي اين فريم ورك‌ها از پروژه FluentNHibernate قابل استخراج هستند.

سپس سه كلاس زير را به پروژه آزمون واحد اضافه خواهيم كرد.
كلاس TestModel : (جهت مشخص سازي محل دريافت اطلاعات نگاشت)

using FluentNHibernate;
using NHSample1.Domain;

namespace UnitTests
{
public class TestModel : PersistenceModel
{
public TestModel()
{
AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
}
}
}

كلاس FixtureBase : (جهت ايجاد سشن NHibernate در ابتداي آزمون واحد و سپس پاكسازي اشياء در پايان كار)

using NUnit.Framework;
using NHibernate;
using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;

namespace UnitTests
{
public class FixtureBase
{
protected SessionSource SessionSource { get; set; }
protected ISession Session { get; private set; }

[SetUp]
public void SetupContext()
{
var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.InMemory);

SessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
new TestModel());

Session = SessionSource.CreateSession();
SessionSource.BuildSchema(Session);
}

[TearDown]
public void TearDownContext()
{
Session.Close();
Session.Dispose();
}
}
}

و كلاس CustomerMapping_Fixture.cs : (جهت بررسي صحت نگاشت تهيه شده با كمك دو كلاس قبل)

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class CustomerMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_customer()
{
new PersistenceSpecification<Customer>(Session)
.CheckProperty(c => c.Id, 1001)
.CheckProperty(c => c.FirstName, "Vahid")
.CheckProperty(c => c.LastName, "Nasiri")
.CheckProperty(c => c.AddressLine1, "Addr1")
.CheckProperty(c => c.AddressLine2, "Addr2")
.CheckProperty(c => c.PostalCode, "1234")
.CheckProperty(c => c.City, "Tehran")
.CheckProperty(c => c.CountryCode, "IR")
.VerifyTheMappings();
}
}
}

توضيحات:
اكنون به عنوان يك برنامه نويس متعهد نياز است تا كار صورت گرفته در قسمت قبل را آزمايش كنيم.
كار بررسي صحت نگاشت تعريف شده در قسمت قبل توسط كلاس استاندارد PersistenceSpecification فريم ورك FluentNHibernate انجام خواهد شد (در كلاس CustomerMapping_Fixture). اين كلاس براي انجام عمليات آزمون واحد نياز به كلاس پايه ديگري به نام FixtureBase دارد كه در آن كار ايجاد سشن NHibernate (در قسمت استاندارد SetUp آزمون واحد) و سپس آزاد سازي آن را در هنگام خاتمه كار ، انجام مي‌دهد (در قسمت TearDown آزمون واحد).
اين ويژگي‌ها كه در مباحث آزمون واحد نيز به آن‌ها اشاره شده است، سبب اجراي متدهايي پيش از اجرا و بررسي هر آزمون واحد و سپس آزاد سازي خودكار منابع خواهند شد.
براي ايجاد يك سشن NHibernate نياز است تا نوع ديتابيس و همچنين رشته اتصالي به آن (كانكشن استرينگ) مشخص شوند. فريم ورك Fluent NHibernate با ايجاد كلاس‌هاي كمكي براي اين امر، به شدت سبب ساده‌ سازي انجام آن شده است. در اين مثال، نوع ديتابيس به SQLite و در حالت ديتابيس در حافظه (in memory)، تنظيم شده است (براي انجام امور آزمون واحد با سرعت بالا).
جهت اجراي هر دستوري در NHibernate نياز به يك سشن مي‌باشد. براي تعريف شيء سشن، نه تنها نياز به مشخص سازي نوع و حالت ديتابيس مورد استفاده داريم، بلكه نياز است تا وهله‌اي از كلاس استاندارد PersistanceModel را نيز جهت مشخص سازي كلاس نگاشت مورد استفاده مشخص نمائيم. براي اين منظور كلاس TestModel فوق تعريف شده است تا اين نگاشت را از اسمبلي مربوطه بخواند و مورد استفاده قرار دهد (بر پايي اوليه اين مراحل شايد در ابتداي امر كمي زمانبر باشد اما در نهايت يك پروسه استاندارد است). توسط اين كلاس به سيستم اعلام خواهيم كرد كه اطلاعات نگاشت را بايد از كدام كلاس دريافت كند.
تا اينجاي كار شيء SessionSource را با معرفي نوع ديتابيس و همچنين محل دريافت اطلاعات نگاشت اشياء معرفي كرديم. در دو سطر بعدي متد SetupContext كلاس FixtureBase ، ابتدا يك سشن را از اين منبع سشن تهيه مي‌كنيم. شيء منبع سشن در اين فريم ورك در حقيقت يك factory object است (الگوهاي طراحي برنامه نويسي شيءگرا) كه امكان دسترسي به انواع و اقسام ديتابيس‌ها را فراهم مي‌سازد. براي مثال اگر روزي نياز بود از ديتابيس اس كيوال سرور استفاده شود، مي‌توان از كلاس MsSqlConfiguration بجاي SQLiteConfiguration استفاده كرد و همينطور الي آخر.
در ادامه توسط شيء SessionSource كار ساخت database schema را نيز به صورت پويا انجام خواهيم داد. بله، همانطور كه متوجه شده‌ايد، كار ساخت database schema نيز به صورت پويا توسط فريم ورك NHibernate با توجه به اطلاعات كلاس‌هاي نگاشت، صورت خواهد گرفت.

اين مراحل، نحوه ايجاد و بر پايي يك آزمايشگاه آزمون واحد فريم ورك Fluent NHibernate را مشخص ساخته و در پروژه‌هاي شما مي‌توانند به كرات مورد استفاده قرار گيرند.

در ادامه اگر آزمون واحد را اجرا نمائيم (متد can_correctly_map_customer در كلاس CustomerMapping_Fixture)، نتيجه بايد شبيه به شكل زير باشد:



توسط متد CheckProperty كلاس PersistenceSpecification ، امكان بررسي نگاشت تهيه شده ميسر است. اولين پارامتر آن، يك lambda expression خاصيت مورد نظر جهت بررسي است و دومين آرگومان آن، مقداري است كه در حين آزمون به خاصيت تعريف شده انتساب داده مي‌شود.

نكته:
شايد سؤال بپرسيد كه در تابع can_correctly_map_customer عملا چه اتفاقاتي رخ داده است؟ براي بررسي آن در متد SetupContext كلاس FixtureBase ، اولين سطر آن‌را به صورت زير تغيير دهيد تا عبارات SQL نهايي توليد شده را نيز بتوانيم در حين عمليات تست مشاهده نمائيم:

var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.ShowSql().InMemory);




مطابق متد تست فوق، عبارات توليد شده به شرح زير هستند:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 1001
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.FirstName as FirstName0_0_, customer0_.LastName as LastName0_0_, customer0_.AddressLine1 as AddressL4_0_0_, customer0_.AddressLine2 as AddressL5_0_0_, customer0_.PostalCode as PostalCode0_0_, customer0_.City as City0_0_, customer0_.CountryCode as CountryC8_0_0_ FROM "Customer" customer0_ WHERE customer0_.Id=@p0;@p0 = 1001

نكته جالب اين عبارات، استفاده از كوئري‌هاي پارامتري است به صورت پيش فرض كه در نهايت سبب بالا رفتن امنيت بيشتر برنامه (يكي از راه‌هاي جلوگيري از تزريق اس كيوال در ADO.Net كه در نهايت توسط تمامي اين فريم ورك‌ها در پشت صحنه مورد استفاده قرار خواهند گرفت) و همچنين سبب بكار افتادن سيستم‌هاي كش ديتابيس‌هاي پيشرفته مانند اس كيوال سرور مي‌شوند (execution plan كوئري‌هاي پارامتري در اس كيوال سرور جهت بالا رفتن كارآيي سيستم كش مي‌شوند و اهميتي هم ندارد كه حتما رويه ذخيره شده باشند يا خير).

ادامه دارد ...