مقدمه:
از آنجایی که در این سایت در مورد shim و stub صحبتی نشده دوست داشتم مطلبی در این باره بزارم. در آزمون واحد ما نیاز داریم که یک سری اشیا را moq کنیم تا بتوانیم آزمون واحد را به درستی انجام دهیم. ما در آزمون واحد نباید وابستگی به لایههای پایین یا بالا داشته باشیم پس باید مقلدی از object هایی که در سطوح مختلف قرار دارند بسازیم.
شاید برای کسانی که با آزمون واحد کار کردند، به ویژه با فریم ورک تست Microsoft، یک سری مشکلاتی با mock کردن اشیا با استفاده از Mock داشته اند که حالا میخواهیم با معرفی فریم ورکهای جدید، این مشکل را حل کنیم.
برای اینکه شما آزمون واحد درستی داشته باشید باید کارهای زیر را انجام دهید:
1- هر objectی که نیاز به mock کردن دارد باید حتما یا non-static باشد، یا اینترفیس داشته باشد.
2- شما احتیاج به یک فریم ورک تزریق وابستگیها دارید که به عنوان بخشی از معماری نرم افزار یا الگوهای مناسب شیءگرایی مطرح است، تا عمل تزریق وابستگیها را انجام دهید.
3- ساختارها باید برای تزریق وابستگی در اینترفیسهای objectهای وابسته تغییر یابند.
Shims و Stubs:
نوع stub همانند فریم ورک mock میباشد که برای مقلد ساختن اینترفیسها و کلاسهای non-sealed virtual یا ویژگی ها، رویدادها و متدهای abstract استفاده میشود. نوع shim میتواند کارهایی که stub نمیتواند بکند انجام دهد یعنی برای مقلد ساختن کلاسهای static یا متدهای non-overridable استفاده میشود. با مثالهای زیر میتوانید با کارایی بیشتر shim و stub آشنا شوید.
یک پروژه mvc ایجاد کنید و نام آن را FakingExample بگذارید. در این پروژه کلاسی با نام CartToShim به صورت زیر ایجاد کنید:
namespace FakingExample
{
public class CartToShim
{
public int CartId { get; private set; }
public int UserId { get; private set; }
private List<CartItem> _cartItems = new List<CartItem>();
public ReadOnlyCollection<CartItem> CartItems { get; private set; }
public DateTime CreateDateTime { get; private set; }
public CartToShim(int cartId, int userId)
{
CartId = cartId;
UserId = userId;
CreateDateTime = DateTime.Now;
CartItems = new ReadOnlyCollection<CartItem>(_cartItems);
}
public void AddCartItem(int productId)
{
var cartItemId = DataAccessLayer.SaveCartItem(CartId, productId);
_cartItems.Add(new CartItem(cartItemId, productId));
}
}
}
و همچنین کلاسی با نام CartItem به صورت زیر ایجاد کنید:
public class CartItem
{
public int CartItemId { get; private set; }
public int ProductId { get; private set; }
public CartItem(int cartItemId, int productId)
{
CartItemId = cartItemId;
ProductId = productId;
}
}
حالا یک پروژه unit test را با نام FakingExample.Tests اضافه کرده و نام کلاس آن را CartToShimTest بگذارید. یک reference از پروژه FakingExample تان به پروژهی تستی که ساخته اید اضافه کنید. برای اینکه بتوانید کلاسهای پروژه FakingExample را shim و یا stub کنید باید بر روی Reference پروژه تان راست کلیک کنید و گزینه Add Fakes Assembly را انتخاب کنید. وقتی این گزینه را میزنید، پوشه ای با نام Fakes در پروژه تست ایجاد شده و FakingExample.fakes در داخل آن قرار دارد همچنین در referenceهای پروژه تست، FakingExample.Fakes نیز ایجاد میشود.
اگر بر روی فایل fakes که در reference ایجاد شده دوبار کلیک کنید میتوانید کلاسهای CartItem و CartToShim را مشاهده کنید که هم نوع stub شان است و هم نوع shim آنها که در تصویر زیر میتوانید مشاهده کنید.
ShimDataAccessLayer را که مشاهده میکنید یک متد SaveCartItem دارد که به دیتابیس متصل شده و آیتمهای کارت را ذخیره میکند.
حالا میتوانیم تست خود را بنویسیم. در زیر یک نمونه از تست را مشاهده میکنید:
[TestMethod]
public void AddCartItem_GivenCartAndProduct_ThenProductShouldBeAddedToCart()
{
//Create a context to scope and cleanup shims
using (ShimsContext.Create())
{
int cartItemId = 42, cartId = 1, userId = 33, productId = 777;
//Shim SaveCartItem rerouting it to a delegate which
//always returns cartItemId
Fakes.ShimDataAccessLayer.SaveCartItemInt32Int32 = (c, p) => cartItemId;
var cart = new CartToShim(cartId, userId);
cart.AddCartItem(productId);
Assert.AreEqual(cartId, cart.CartItems.Count);
var cartItem = cart.CartItems[0];
Assert.AreEqual(cartItemId, cartItem.CartItemId);
Assert.AreEqual(productId, cartItem.ProductId);
}
}
همانطور که در بالا مشاهده میکنید کدهای تست ما در اسکوپی قرار گرفته اند که محدوده shim را تعیین میکند و پس از پایان یافتن تست، تغییرات shim به حالت قبل بر میگردد. متد SaveCartItemInt32Int32 را که مشاهده میکنید یک متد static است و نمیتوانیم با mock ویا stub آن را مقلد کنیم. تغییر اسم متد SaveCartItem به SaveCartItemInt32Int32 به این معنی است که متد ما دو ورودی از نوع Int32 دارد و به همین خاطر fake این متد به این صورت ایجاد شده است. مثلا اگر شما متد Save ای داشتید که یک ورودی Int و یک ورودی String داشت fake آن به صورت SaveInt32String ایجاد میشد.
به این نکته توجه داشته باشید که حتما برای assert کردن باید assertها را در داخل اسکوپ ShimsContext قرار گرفته باشد در غیر این صورت assert شما درست کار نمیکند.
این یک مثال از shim بود؛ حالا میخواهم مثالی از یک stub را برای شما بزنم. یک اینترفیس با نام ICartSaver به صورت زیر ایجاد کنید:
public interface ICartSaver
{
int SaveCartItem(int cartId, int productId);
}
برای shim کردن ما نیازی به اینترفیس نداشتیم اما برای استفاده از stub و یا Mock ما حتما به یک اینترفیس نیاز داریم تا بتوانیم object موردنظر را مقلد کنیم. حال باید یک کلاسی با نام CartSaver برای پیاده سازی اینترفیس خود بسازیم:
public class CartSaver : ICartSaver
{
public int SaveCartItem(int cartId, int productId)
{
using (var conn = new SqlConnection("RandomSqlConnectionString"))
{
var cmd = new SqlCommand("InsCartItem", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@CartId", cartId);
cmd.Parameters.AddWithValue("@ProductId", productId);
conn.Open();
return (int)cmd.ExecuteScalar();
}
}
}
حال تستی که با shim انجام دادیم را با استفاده از Stub انجام میدهیم:
[TestMethod]
public void AddCartItem_GivenCartAndProduct_ThenProductShouldBeAddedToCart()
{
int cartItemId = 42, cartId = 1, userId = 33, productId = 777;
//Stub ICartSaver and customize the behavior via a
//delegate, ro return cartItemId
var cartSaver = new Fakes.StubICartSaver();
cartSaver.SaveCartItemInt32Int32 = (c, p) => cartItemId;
var cart = new CartToStub(cartId, userId, cartSaver);
cart.AddCartItem(productId);
Assert.AreEqual(cartId, cart.CartItems.Count);
var cartItem = cart.CartItems[0];
Assert.AreEqual(cartItemId, cartItem.CartItemId);
Assert.AreEqual(productId, cartItem.ProductId);
}
امیدوارم که این مطلب برای شما مفید بوده باشد.