۳۰ روز با TDD: روز هجدهم - بازبینی Refactoring قسمت اول

داستان ۳۰ روز با TDD

سپتامبر سال ۲۰۱۳ آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما هجدهمین روز: بازبینی Refactoring

توجه: قبل از این نوشته، آزمون‌های واحد (Unit testها) مربوط به تغییرات PlaceOrder نوشته قبلی را از اینجا می‌توانید دانلود کنید.

در چند نوشته گذشته، متد PlaceOrder را از OrderService بیرون بردیم. برای مرور، متد فعلی این شکلی است:


public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            foreach (var item in shoppingCart.Items)
            {
                if (item.Quantity == 0)
                {
                    throw new InvalidOrderException();
                }
            }

            var customer = _customerService.GetCustomer(customerId);

            //Open Session
            var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
            
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Order
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId, 
                orderForFulfillmentService, 
                customer.ShippingAddress.ToString());
                       
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);

            var order = new Order();
            return _orderDataService.Save(order);
        }

این متد کمی طولانی شده و همچنین داریم به محدوده نقض Single Responsibility Principel (برای مرور SRP روز پنجم را مطالعه کنید) وارد می‌شویم. در حال حاضر شش دلیل برای اینکه این متد باید تغییر کند شمردم:

۱. اعتبارسنجی هر آیتم سبد خرید
۲. روش باز شدن session ها با سرویس order fulfillment
۳. روش چک کردن انبار با سرویس order fulfillment
۴. روش ثبت سفارش با سرویس order fulfillment
۵. بسته شدن session ها با order fulfillment
۶. ایجاد و ذخیره یک سفارش

در لیست بالا «ایجاد و ذخیره یک سفارش» موردی است که کمترین نگرانی را درباره‌اش دارم. کد زیادی درباره ایجاد و ذخیره سفارش نیست پس فعلاً جای نگرای نیست. این موضوع بعداً در جریان توسعه نرم‌افزار قطعاً تغییر می‌کند اما فعلاً در رادار من نیست. مورد اول هم گرچه جای نگرانی دارد ولی در حال حاضر بدترین مشکل نقض SRP نیست، بنابراین می‌تواند منتظر بماند.

چیزی که الان می‌خواهم رویش تمرکز کنم موارد ۲ تا ۵ هستند. همه این موارد با سرویس order fulfillment در ارتباطند و بخش زیادی از کد را تشکیل می‌دهند.

توسعه‌دهنده‌هایی که درباره ایده SRP یا refactoring تازه‌وارد هستند احتمالاً به لیست بالا نگاه می‌کنند و فکر می‌کنند احتمالاً به ۴ متد جدید برای هر یک از مسائل ۲ تا ۵ نیاز داریم. این اشتباه نیست، اما بخشی از داستان است. اینکه خیلی ساده ۴ متد جدید اضافه کنیم، کد را خواناتر و قابل استفاده مجددتر می‌کند، اما همچنین شش دلیل برای تغییر PlaceOrder خواهیم داشت. SRP‌ و refactoring فقط این نیست که کدها را به متدهای خصوصی (private methods) ببریم تا متدهای اصلی را کوتاه‌تر کنیم، بلکه درباره ایجاد تجرد منطقی (logical abstraction) در کد است تا مرا در برابر تغییرات آینده محافظت کند.

آنچه می‌بینم، حجم زیادی پروسه کار با سرویس order fulfillment است که می‌خواهم از متد PlaceOrder جدایش کنم. وقتی این کار انجام شد، برای کل این پروسه طولانی کار با order fulfillment فقط یک متد باید فراخوانی کنم. این کار مرا در برابر تغییرات مرتبط در هر یک از مراحل کار با سرویس order fulfillment محافظت می‌کند. پس قدم اول استخراج همه کدهایی که با سرویس order fulfillment کار می‌کنند برای ایجاد یک متد خصوصی جداگانه است. چون از Just Code (محصول شرکت تلریک) استفاده می‌کنم این کار ساده است: می‌توانم یک بلوک کد را انتخاب و از منوی کمکی Just Code گزینه Extract Method را مشابه تصویر زیر انتخاب کنم یا از کلید میانبر (CTRL + R, CTRL + M) استفاده کنم و Just Code بخش انتخاب شده را برای یک متد خصوصی جدید استخراج می‌کند.

Just Code کد مورد نیاز را به همراه پارامترهای مورد نیاز استخراج می‌کند و از من درباره نام متد سوال می‌کند و من هم نام PlaceOrdeWithFulfillmentService را انتخاب می‌کنم. همچنین کد اصلی را با فراخوانی این متد تازه ساخته شده تغییر می‌دهد.

public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            foreach (var item in shoppingCart.Items)
            {
                if (item.Quantity == 0)
                {
                    throw new InvalidOrderException();
                }
            }

            var customer = _customerService.GetCustomer(customerId);

            PlaceOrderWithFulfillmentService(shoppingCart, customer);

            var order = new Order();
            return _orderDataService.Save(order);
        }
  
        private void PlaceOrderWithFulfillmentService(ShoppingCart shoppingCart, Customer customer)
        {
            //Open Session
            var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
              
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Orders
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
                orderForFulfillmentService,
                customer.ShippingAddress.ToString());
                         
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
        }

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

نتیجه اجرای تست‌ها به این معنی است که اولین refactor من کار کرده و نوع بازگشتی متد PlaceOrderWithFulfillmentService فعلاً می‌تواند void باشد. حالا اجازه بدهید به سراغ متد تازه ایجاد شده برویم و refactor کنیم.

متد PlaceOrderWithFulfillmentService باعث شده که کد ما کمتر شکننده باشد، اما خودش هم مشکلاتی دارد. ما به refactor بیشتری نیاز داریم و حالا می‌خواهم هر یک از مراحل را به متدهای خودشان منتقل کنم. به سراغ اولین مرحله می‌روم: OpenOrderFuilfillmentService


private void PlaceOrderWithFulfillmentService(ShoppingCart shoppingCart, Customer customer)
        {
            //Open Session
            var orderFulfillmentSessionId = OpenOrderFulfillmentSession();
              
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Orders
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
                orderForFulfillmentService,
                customer.ShippingAddress.ToString());
                         
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
        }
  
        private Guid OpenOrderFulfillmentSession()
        {
            var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
            return orderFulfillmentSessionId;
        }

اجرای تست‌ها نشان می‌دهد که این refactor هم موفق بوده است. حالا مرحله بستن session را هم به متد جدیدی به نام CloseOrderFulfillmentService می‌برم

private void PlaceOrderWithFulfillmentService(ShoppingCart shoppingCart, Customer customer)
        {
            //Open Session
            var orderFulfillmentSessionId = OpenOrderFulfillmentSession();
              
            var firstItemId = shoppingCart.Items[0].ItemId;
            var firstItemQuantity = shoppingCart.Items[0].Quantity;

            //Check Inventory Level
            var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);

            //Place Orders
            var orderForFulfillmentService = new Dictionary();
            orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
            var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId,
                orderForFulfillmentService,
                customer.ShippingAddress.ToString());
                         
            //Close Session
            CloseOrderFulfillmentService(orderFulfillmentSessionId);
        }
  
        private void CloseOrderFulfillmentService(Guid orderFulfillmentSessionId)
        {
            //Close Session
            _orderFulfillmentService.CloseSession(orderFulfillmentSessionId);
        }

اجرای تست‌ها نشان می‌دهد که همچنان کد بدون مشکل کار می‌کند.

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

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

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز هفدهم-تعیین ترتیب اجرا در mock ها

داستان ۳۰ روز با TDD

سپتامبر سال ۲۰۱۳ آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما هفدهیمن روز: تعیین ترتیب اجرای mock ها

امروز هم توسعه برنامه فروشگاهی که به روش TDD نوشتیم را با نگاهی نزدیک‌تر به سرویس Order Fulfillment که یک سرویس خارج از برنامه اصلی است، ادامه می‌دهیم. 

ارسال سفارش

اگر نوشته قبلی را خوانده باشید، به یاد دارید که ما با یک سرویس خارج از برنامه اصلی برای اجرای سفارشات کار می‌کردیم. آن‌ها یک API فراهم کرده‌اند و ما OrderFulfillmentService را فراخوانی می‌کنیم. interface این API چندین فراخوانی و مجموعه‌ای از قوانین برای ترتیب فراخوانی‌ها دارد. خلاصه‌ای از این interface را در کد زیر می‌توانید ببینید:

using System;
using System.Collections.Generic;




namespace TddStore.Core
{
    public interface IOrderFulfillmentService
    {
        Guid OpenSession(string user, string password);




        bool IsInInventory(Guid sessionId, Guid ItemNumber, int quantity);




        bool PlaceOrder(Guid sessionId, IDictionary items, string mailingAddress);




        void CloseSession(Guid sessionId);
    }
}

می‌توانیم درباره کارآیی این interface بحث کنیم، اما به هر حال این interface ای است که شریک اجرایی ما از آن استفاده می‌کند و بنابراین ما هم باید از همین interface استفاده کنیم. چند قانون پیرامون گردش کار این API وجود دارد:

  • کاربر باید برای هر سفارش از یک session استفاده کند
  • کاربر برای باز کردن session باید نام کاربری و کلمه عبور خود را ارائه دهد
  • همه فراخوانی‌های سرویس باید شامل شناسه (id) مربوط به session باشند
  • کاربر باید تایید کند که همه اقلام سفارش، قبل از ثبت سفارش در انبار وجود دارد. ثبت سفارش اقلامی که در انبار نیستند باعث شکست ثبت سفارش می‌شود
  • کاربر باید session را به جهت تکمیل سفارش خاتمه دهد
  • شناسه session کد رهگیری سفارش خواهد بود
چند قید (constraint) در این قوانین وجود دارد که باید روی آن‌ها کار کنیم. در نوشته امروز به همه آن‌ها نخواهیم پرداخت، اما روی ترتیب عملیات مربوط به این قیدها کار خواهم کرد. اولین چیزی که نیاز داریم یک تست کیس (test case) است
وقتی که سفارشی ثبت می‌شود که در انبار موجود است، گردش کار اجرای سفارش باید کامل شود
درست است. این یک تست کیس بسیار ساده است. این تست روی هدف نوشته امروز بسیار متمرکز شده، که در واقع اطمینان از این مطلب است که فراخوانی‌های OrderFulfillment به ترتیب انجام شوند. 
[Test]
public void WhenUserPlacesOrderWithItemThatIsInInventoryOrderFulfillmentWorkflowShouldComplete()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
	var customerId = Guid.NewGuid();
	var customer = new Customer { Id = customerId };




	Mock.Arrange(() => _customerService.GetCustomer(customerId)).Returns(customer).OccursOnce();
}
این‌ها موارد ابتدایی فراخوانی PlaceOrder در OrderService هستند همانطور که در نوشته قبلی اشاره کردم، باید برای دریافت اطلاعات مشتری (از جمله آدرس وی) CustomerService را فراخوانی کنم. حالا به arrange کردن mock نیازمندم. تغییر کوچکی در arrange فعلی انجام می‌دهم و بخش Act را با فراخوانی متد PlaceOrder از OrderService می‌سازم.
[Test]
public void WhenUserPlacesOrderWithItemThatIsInInventoryOrderFulfillmentWorkflowShouldComplete()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	var itemId = Guid.NewGuid();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = itemId, Quantity = 1 });
	var customerId = Guid.NewGuid();
	var customer = new Customer { Id = customerId };
	var orderFulfillmentSessionId = Guid.NewGuid();




	Mock.Arrange(() => _customerService.GetCustomer(customerId)).Returns(customer).OccursOnce();




	Mock.Arrange(() => _orderFulfillmentService.OpenSession(Arg.IsAny(), Arg.IsAny()))
		.Returns(orderFulfillmentSessionId)
		.InOrder();




	//Act
	_orderService.PlaceOrder(customerId, shoppingCart);
	
	//Assert
	Mock.Assert(_orderFulfillmentService);
}
بیشتر کدی که نوشتم باید واضح باشد. در خط ۱۰ ، مقداری جهت ایجاد و گرفتن session id مربوط به تکمیل سفارش اضافه کردم و در خط ۲۲ اجرای صحیح mock را assert کردم. نکته جالب این کد در خط ۱۶ است که ثابت InOrder را اضافه کردم. بخش arrange کد را تکمیل می‌کنیم:
[Test]
public void WhenUserPlacesOrderWithItemThatIsInInventoryOrderFulfillmentWorkflowShouldComplete()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	var itemId = Guid.NewGuid();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = itemId, Quantity = 1 });
	var customerId = Guid.NewGuid();
	var customer = new Customer { Id = customerId };
	var orderFulfillmentSessionId = Guid.NewGuid();




	Mock.Arrange(() => _customerService.GetCustomer(customerId)).Returns(customer).OccursOnce();




	Mock.Arrange(() => _orderFulfillmentService.OpenSession(Arg.IsAny(), Arg.IsAny()))
		.Returns(orderFulfillmentSessionId)
		.InOrder();
	Mock.Arrange(() => _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, itemId, 1))
		.Returns(true)
		.InOrder();
	Mock.Arrange(() => 
		_orderFulfillmentService.
			PlaceOrder(orderFulfillmentSessionId, Arg.IsAny>(), Arg.IsAny()))
		.Returns(true)
		.InOrder();
	Mock.Arrange(() => _orderFulfillmentService.CloseSession(orderFulfillmentSessionId))
		.InOrder();




	//Act
	_orderService.PlaceOrder(customerId, shoppingCart);
	
	//Assert
	Mock.Assert(_orderFulfillmentService);
}
در کد بالا، در خط‌های ۱۴ تا ۲۶ من mock برای OrderFulfillmentService را arrange می‌کنم. نگران نباشید و حواستان با لیست پارامترهای mock پرت نشود، با ایجاد سفارشی این پارامترها در نوشته بعدی آشنا خواهیم شد. الان فقط روی ترتیب متدها متمرکز می‌شویم. متوجه شدید که mock ها را به ترتیبی که می‌خواهم متدها اجرا شوند arrange کرده‌ام. OpenSession –> IsInInventory –> PlaceOrder –> CloseSession اگر برای فراخوانی این‌ها به ترتیب دیگری عمل کنم، تست من موقع فراخوانی MockAssert حتماً fail خواهد شد.
اگر برای اجرای این تست تلاش کنم، خطای کامپایلر برای عدم تعریف _orderFulfillmentService دریافت می‌کنم. تا اینجای کار به سرویس OrdeFulfillment نیازی نداشتم ولی حالا به mock نیاز دارم:
namespace TddStore.UnitTests
{
    [TestFixture]
    class OrderServiceTests
    {
        private OrderService _orderService;
        private IOrderDataService _orderDataService;
        private ICustomerService _customerService;
        private IOrderFulfillmentService _orderFulfillmentService;




        [TestFixtureSetUp]
        public void SetupTestFixture()
        {
            _orderDataService = Mock.Create();
            _customerService = Mock.Create();
            _orderFulfillmentService = Mock.Create();
            _orderService = new OrderService(_orderDataService, _customerService);
        }
بسیار خب. نوبت اجرای تست رسیده

تست ما fail شد چرا که هیچیک از متدهای OrderFulfillmentService  فراخوانی نشده‌اند. قدم بعدی، پیاده‌سازی کد برای این تست است
  public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
{
	foreach (var item in shoppingCart.Items)
	{
		if (item.Quantity == 0)
		{
			throw new InvalidOrderException();
		}
	}




	var customer = _customerService.GetCustomer(customerId);




	//Open Session
	var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
	
	var firstItemId = shoppingCart.Items[0].ItemId;
	var firstItemQuantity = shoppingCart.Items[0].Quantity;
	
	//Place Order
	var orderForFulfillmentService = new Dictionary();
	orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
	var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId, 
		orderForFulfillmentService, 
		customer.ShippingAddress.ToString());




	//Check Inventory Level
	var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);




	
	//Close Session
	_orderFulfillmentService.CloseSession(orderFulfillmentSessionId);




	var order = new Order();
	return _orderDataService.Save(order);
}
در خط ۱۴ شروع کردم به کار با OrderFulFillmentService و برای اینکه بدانیم در هر مرحله چه کاری انجام می‌شود در کد، توضیح (comment) گذاشته‌ام. حالا توجه کنید که گام‌های فراخوانی من به ترتیب مورد انتظار نیست. من از باز کردن session مستقیماً به سفارش می‌روم به جای اینکه اول موجودی انبار را چک کنم. می‌توانم تلاش کنم تست را اجرا کنم ولی به دلیل نبود وهله (instance) از OrderFulfillmentService خطای کامپایل دریافت می‌کنم، همچنین سازنده کلاس را برای تزریق وابستگی به روز نکرده‌ام. حالا این دو مشکل را حل می‌کنم. در حالی که در مشغول این کار هستم، ثابت‌های نام کاربری و کلمه عبور را هم تعریف می‌کنم
namespace TddStore.Core
{
    public class OrderService
    {
        private IOrderDataService _orderDataService;
        private ICustomerService _customerService;
        private IOrderFulfillmentService _orderFulfillmentService;
        private const string USERNAME = "Bob";
        private const string PASSWORD = "Foo";
        




        public OrderService(IOrderDataService orderDataService, ICustomerService customerService, 
            IOrderFulfillmentService orderFulfillmentService)
        {
            _orderDataService = orderDataService;
            _customerService = customerService;
            _orderFulfillmentService = orderFulfillmentService;
        }
تلاش برای اجرای کد، به خطاهای کامپایل بیشتری منجر می‌شود چرا که تست من برای تزریق mock مربوط به OrderFulFillmentService  به عنوان سومین آرگومان OrderService به روز نشده. این به روز رسانی را هم انجام می‌دهم
 [TestFixtureSetUp]
public void SetupTestFixture()
{
	_orderDataService = Mock.Create();
	_customerService = Mock.Create();
	_orderFulfillmentService = Mock.Create();
	_orderService = new OrderService(_orderDataService, _customerService, _orderFulfillmentService);
}
برنامه در نهایت کامایل می‌شود. حالا نوبت اجرای تست است و مجدداً fail می‌شود!

این نتیجه غیرمنتظره نبود، اگر به یاد داشته باشید به عمد متدها را با ترتیب غیرصحیحی در OrderFulfillmentService فراخوانی کردم. نرم‌افزار (در این سری نوشته‌ها JustMock) به ما می‌گوید که ترتیب فراخوانی متدها صحیح نیست و لیست ترتیب فراخوانی را هم نشان می‌دهد. با جابجایی فراخوانی متدها به کد زیر می‌رسیم:
 public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
{
	foreach (var item in shoppingCart.Items)
	{
		if (item.Quantity == 0)
		{
			throw new InvalidOrderException();
		}
	}




	var customer = _customerService.GetCustomer(customerId);




	//Open Session
	var orderFulfillmentSessionId = _orderFulfillmentService.OpenSession(USERNAME, PASSWORD);
	
	var firstItemId = shoppingCart.Items[0].ItemId;
	var firstItemQuantity = shoppingCart.Items[0].Quantity;




	//Check Inventory Level
	var itemIsInInventory = _orderFulfillmentService.IsInInventory(orderFulfillmentSessionId, firstItemId, firstItemQuantity);




	//Place Order
	var orderForFulfillmentService = new Dictionary();
	orderForFulfillmentService.Add(firstItemId, firstItemQuantity);
	var orderPlaced = _orderFulfillmentService.PlaceOrder(orderFulfillmentSessionId, 
		orderForFulfillmentService, 
		customer.ShippingAddress.ToString());
			   
	//Close Session
	_orderFulfillmentService.CloseSession(orderFulfillmentSessionId);




	var order = new Order();
	return _orderDataService.Save(order);
}
نوبت اجرای مجدد تست است. 

با اطمینان یافتن از اینکه متدها با ترتیب درستی فراخوانی می‌شوند، می‌توانم مطمئن شوم که تستم pass خواهد شد.
ادامه دارد ...
به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز شانزدهم- استفاده از پارامترهای مشخص در Stub ها

داستان ۳۰ روز با TDD

سپتامبر سال گذشته آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما شانزدمین روز: استفاده از پارامترهای مشخص در Stub ها

ما مثال چند نوشته قبلی این سری نوشته‌ها را پی خواهیم گرفت. نیازمندی نرم‌افزاری را در روز دوازدهم از Stub بیان کردیم. به عنوان بخشی از این نیازمندی، ما باید با یک سیستم ثبت سفارش خارجی ارتباط برقرار کنیم. تست کیس بعدی که می‌خواهم به آن بپردازم مربوط به همین ارتباط با سیستم خارجی ثبت سفارش است. به جای اینکه خودمان بخش ثبت سفارش را بنویسیم، مدیریت تصمیم گرفته که آن را برون‌سپاری (outsource) کنیم. این تصمیم به آن معنی است که برای بخش ثبت سفارش باید با یک وب سرویس خارج از نرم‌افزار اصلی ارتباط برقرار کنیم. همچنین برای این ارتباط باید بخشی از اطلاعات مشتری (حداقل نام و آدرس) را آماده کنیم.

برای ارائه اطلاعات مشتری (اعم از نام و آدرس) CustomerService می‌بایست تغییر کند.

using System;
namespace TddStore.Core
{
    public class Customer
    {
        public Guid Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Address ShippingAddress { get; private set; }




        public Customer()
        {
            ShippingAddress = new Address();
        }
    }




    public class Address
    {
        public string StreetAddressOne { get; set; }
        public string StreetAddressTwo { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
    }
}

سپس به یک Interface برای Customer Service نیاز داریم

using System;




namespace TddStore.Core
{
    public interface ICustomerService
    {
        Customer GetCustomer(Guid customerId);
    }
}

هم تعریف Entity و هم Interface مربوط به Customer Service را به پروژه TddStore.Core اضافه می‌کنیم.

در این قسمت باید از اولین تست کیس مربوط به تعامل با OrderFulfillment Service صحبت کنم که جالب است بدانید هیچ ارتباطی با خود OrderFulfillment ندارد! در این مورد می‌خواهم مطمئن شوم که OrderService به درستی از CustomerService استفاده می‌کند.

صورت مساله تست: وقتی یک مشتری معتبر سفارشی را ثبت می‌کند (که اعتبارسنجی آن انجام شده) این سفارش ثبت شده و یک شناسه سفارش برگردانده می‌شود

به یک تست جدید نیاز دارم:


[Test]
        public void WhenAValidCustomerPlacesAValidOrderAnOrderShouldBePlaced()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
            var customerId = Guid.NewGuid();
            
            //Act
            _orderService.PlaceOrder(customerId, shoppingCart);
        }

بلی! این شبیه شروع تست WhenUserPlacesACorrectOrderThenAnOrderNumberShouldBeReturned  به نظر می‌رسد و خیر! نباید WhenUserPlacesACorrectOrderThenAnOrderNumberShouldBeReturned را تغییر دهیم که در کنار تست اصلی، تعامل با CustomService را نیز تست کند.

هدف تست اصلی این است که تعامل با OrderDataSevice به درستی کار می‌کند. جزئیات این تعامل بسته به اینکه متد چطور با CustomerService کار می‌کند ممکن است متفاوت باشد. با تکامل این تست‌ها، نیاز به mock‌های بیشتر OrderDataService حس می‌شود. این باعث می‌شود که هر دو تعامل (از OrderService به OrderDataService و از OrderService به CustomerService) تقریباً مستقل از هم تغییر کنند که باعث تولید تست‌های شکننده کمتری می‌شود. همچنین با استفاده از این روش، چنانچه تستی fail شود دقیقاً خواهم دانست که آیا ارتباط بین OrderDataService و CustomerService باعث این fail شده یا خیر.

قبل از اینکه جلوتر برویم، لازم است وهله (instance) از mock مربوط به CustomerService را در کلاس OrderServiceTests معرفی کنم. همچنین باید از SetupTestFixture نیز استفاده کنم.

    [TestFixture]
    class OrderServiceTests
    {
        private OrderService _orderService;
        private IOrderDataService _orderDataService;
        private ICustomerService _customerService;

        [TestFixtureSetUp]
        public void SetupTestFixture()
        {
            _orderDataService = Mock.Create();
            _customerService = Mock.Create();
            _orderService = new OrderService(_orderDataService, _customerService);
        }

می‌دانم که برای نیازمندی مطرح شده در OrderFultillment باید مشتری را از CustomerService بازیابی (retrieve) کنم. در حال حاضر mock مربوط به CustomerService را تعریف کرده‌ام، پس حالا باید arrange انجام شود. در این مورد می‌خواهم stub من یک مشتری مشخص را وقتی که GetCustomer با پارامتر خاصی فراخوانی می‌شود برگرداند.

           [Test]
        public void WhenAValidCustomerPlacesAValidOrderAnOrderShouldBePlaced()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
            var customerId = Guid.NewGuid();
            var customerToReturn = new Customer { Id = customerId, FirstName = "Fred", LastName = "Flinstone" };

            //Act
            _orderService.PlaceOrder(customerId, shoppingCart);
        }

در ایجاد شی customerToReturn من شی Customer را با Id و نام و نام خانوادگی پر کردم. از نظر فنی در حال حاضر نیازی به نام و نام خانوادگی در این شی ندارم، اما می‌خواهم تا جای ممکن فیلدهای بیشتری را برای تست پر کنم. حالا که ورودی و خروجی تعریف شده‌اند، می‌توانم stub را arrange کنم. در این مورد فقط می‌خوام تا stub مربوط به CustomerService مقدار cutsomterToReturn را وقتی که GetCustomer با پارامتر CustomerId فراخوانی می‌شود برگرداند. همچنین انتظار دارم این متد تنها یک بار فراخوانی شود

        [Test]
        public void WhenAValidCustomerPlacesAValidOrderAnOrderShouldBePlaced()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
            var customerId = Guid.NewGuid();
            var expectedOrderId = Guid.NewGuid();
            var customerToReturn = new Customer { Id = customerId, FirstName = "Fred", LastName = "Flinstone" };


            Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
                .Returns(expectedOrderId)
                .OccursOnce();


            Mock.Arrange(() => _customerService.GetCustomer(customerId))
                .Returns(customerToReturn)
                .OccursOnce();


          //Act
            var result = _orderService.PlaceOrder(customerId, shoppingCart);


            //Assert
            Assert.AreEqual(expectedOrderId, result);
            Mock.Assert(_orderDataService);
        }

در پایان لازم است فراخوانی صحیح mock جدید را با assert چک کنم. این کار را با فراخوانی Mock.Assert و پاس دادن mock مربوط به CustomerService می‌توانم انجام بدهم.

        [Test]
        public void WhenAValidCustomerPlacesAValidOrderAnOrderShouldBePlaced()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
            var customerId = Guid.NewGuid();
            var customerToReturn = new Customer { Id = customerId, FirstName = "Fred", LastName = "Flinstone" };

            Mock.Arrange(() => _customerService.GetCustomer(customerId))
                .Returns(customerToReturn)
                .OccursOnce();

            //Act
            _orderService.PlaceOrder(customerId, shoppingCart);


            //Assert
            Mock.Assert(_customerService);
        }

همانطور که انتظار داریم تست هنگام اجرا fail می‌شود

کد stub‌ مربوط به CustomerService من فراخوانی نشده. باید به متد PlaceOrder در کلاس OrderService بروم و منطق فراخوانی CustomerService را پیاده سازی کنم.

        public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            foreach (var item in shoppingCart.Items)
            {
                if (item.Quantity == 0)
                {
                    throw new InvalidOrderException();
                }
            }
            var customer = _customerService.GetCustomer(customerId);
            var order = new Order();
            return _orderDataService.Save(order);
        }

در این قسمت نمی‌توانم کدم را اجرا کنم چون کامپایل نمی‌شود. ارجاع (reference) به CustomerService در کلاس OrderService ندارم. پس گام بعدی تغییر OrderService برای پذیرش ICustomerService به عنوان یک وابستگی است که از طریق سازنده (constructor) آن را تزریق می‌کنم (برای این تغییر کد راحت‌تر باشد متد PlaceOrder را از نمونه کد زیر برداشتم)

 using System;
using TddStore.Core.Exceptions;


namespace TddStore.Core
{
    public class OrderService
    {
        private IOrderDataService _orderDataService;
        private ICustomerService _customerService;

        public OrderService(IOrderDataService orderDataService, ICustomerService customerService)
        {
            _orderDataService = orderDataService;
            _customerService = customerService;
        }


        public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            foreach (var item in shoppingCart.Items)
            {
                if (item.Quantity == 0)
                {
                    throw new InvalidOrderException();
                }
            }

            var order = new Order();
            return _orderDataService.Save(order);
        }
    }
}

اجرای مجدد تست‌ها نشان می‌دهد که کد همچنان کامپایل نمی‌شود. دلیلش این است که در متد SetupTestFixture کلاس OrderServiceTests تلاش می‌کنیم وهله (instance) از OrderService بدون پاس دادن ICustomerService به سازنده (constrcutor) کلاس بسازیم. برای این کار متد را برای تامین mock در سازنده OrderService تغییر دادم (برای مشاهده راحت‌تر تغییر کد، using ها و متدهای تست را از کلاس برداشتم)

using System;
using System.Linq;
using NUnit.Framework;
using TddStore.Core;
using TddStore.Core.Exceptions;
using Telerik.JustMock;

namespace TddStore.UnitTests
{
    [TestFixture]
    class OrderServiceTests
    {
        private OrderService _orderService;
        private IOrderDataService _orderDataService;
        private ICustomerService _customerService;

        [TestFixtureSetUp]
        public void SetupTestFixture()
        {
            _orderDataService = Mock.Create();
            _customerService = Mock.Create();
            _orderService = new OrderService(_orderDataService, _customerService);
        }


        [Test]
        public void WhenUserPlacesACorrectOrderThenAnOrderNumberShouldBeReturned()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
            var customerId = Guid.NewGuid();
            var expectedOrderId = Guid.NewGuid();


            Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
                .Returns(expectedOrderId)
                .OccursOnce();
            
            //Act
            var result = _orderService.PlaceOrder(customerId, shoppingCart);


            //Assert
            Assert.AreEqual(expectedOrderId, result);
            Mock.Assert(_orderDataService);
        }


        [Test]
        public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
            var customerId = Guid.NewGuid();
            var expectedOrderId = Guid.NewGuid();
            
            Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
                .Returns(expectedOrderId)
                .OccursNever();


            //Act
            try
            {
                _orderService.PlaceOrder(customerId, shoppingCart);
            }
            catch(InvalidOrderException ex)
            {
                //Assert
                Mock.Assert(_orderDataService);
                Assert.Pass();
            }



            //Assert
            Assert.Fail();
        }
    }
}

حالا تست آماده اجراست و همانطور که در تصویر زیر مشخص است، pass می‌شود.

چیزی که ممکن است سوال کنید این است که چرا مشابه تست اصلی، نتیجه فراخوانی PlaceOrder را دریافت و آزمایش نکردیم. دلیلش این است که در این تست، ما اهمیتی به شماره سفارش نمی‌دهیم و فقط تعامل با CustomerService را مورد آزمایش قرار می‌دهیم.

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

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز پانزدهم - ساده همیشه به معنی واضح نیست قسمت دوم

داستان ۳۰ روز با TDD

سپتامبر سال گذشته آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما پانزدهمین روز: ساده همیشه به معنی واضح نیست قسمت دوم

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

مشکل کجاست؟

در روز چهاردهم این تست کیس را دریافت کردیم:

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

از تست کیس بالا به کد تست زیر رسیدیم:

[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
	var customerId = Guid.NewGuid();
	var expectedOrderId = Guid.NewGuid();

	Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
		.Returns(expectedOrderId)
		.OccursNever();

	//Act
	_orderService.PlaceOrder(customerId, shoppingCart);

	//Assert
	Mock.Assert(_orderDataService);
}
}

تست pass‌ شد، اما هنوز یک مشکل وجود دارد. می‌خواهیم مطمئن شویم که متد PlaceOrder در کلاس OrderService یک InvalidOrderException ایجاد می‌کند و متد Save کلاس OrderDataService را فراخوانی نخواهد کرد. به نظر می‌رسد تست این اعتبارسنجی را انجام می‌دهد. ما به تست گفته‌ایم که انتظار InvalidOrderException داشته باشد و stub را طوری تعریف کرده‌ایم که متد Save فراخوانی نشود. تست pass می‌شود، پس مشکل کجاست؟

مشکل اینجاست که تست pass می‌شود، اما فراخوانی Mock.Assert هرگز اتفاق نمی‌افتد. باور نمی‌کنید؟ بیایید یک آزمایش انجام دهیم. بیایید فراخوانی OccursNever را به OccursOnce تغییر دهیم.

[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
//Arrange
var shoppingCart = new ShoppingCart();
shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
var customerId = Guid.NewGuid();
var expectedOrderId = Guid.NewGuid();

Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
.Returns(expectedOrderId)
.OccursOnce();

//Act
_orderService.PlaceOrder(customerId, shoppingCart);

//Assert
Mock.Assert(_orderDataService);
}
}

اگر تست آنطوری که انتظار داریم اجرا شود باید fail شود.

به نظر می‌رسد تست fail نشد. مشخصاً Mock.Assert فراخوانی نمی‌شود.

مشخص شد که مشکل از NUnit است یا به عبارت بهتر از خاصیت ExceptedException در NUnit. این خاصیت به تست می‌گوید که کل کد تست را درون یک بلاک try/catch قرار دهد و یک نوع خاص exception را بگیرد. فراخوانی PlaceOrder در خط ۱۶ باعث بروز exception می‌شود. از آنجایی که ما این exception را handle نکرده‌ایم به سطح اجرا کننده تست (NUnit) می‌رود. به محض اینکه اجرا کننده تست exception ای که منتظرش بوده را دریافت کند تست را pass می‌کند و هیچ خط کد دیگری بعد از بروز exception را اجرا نمی‌کند.

در بسیاری از موارد استفاده از ExpectedException خوب است. ما حالا به موقعیتی برخورد کردیم که استفاده از این خاصیت خوب نیست پس باید به دنبال راه جایگزین و دستی‌تری باشیم. اولین چیزی که لازم است انجام دهم، حذف ExpectedException از کد است. سپس باید فراخوانی PlaceOrder را در یک try/catch قرار بدهم.

[Test]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
	var customerId = Guid.NewGuid();
	var expectedOrderId = Guid.NewGuid();

	Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
		.Returns(expectedOrderId)
		.OccursOnce();

	//Act
	try
	{
		_orderService.PlaceOrder(customerId, shoppingCart);
	}
	catch(InvalidOrderException ex)
	{
		//Assert
		Mock.Assert(_orderDataService);
		Assert.Pass();
	}

	//Assert
	Assert.Fail();
}

برای تایید کد من نوع خاص InvalidOrderExpection را در catch متد PlaceOrder می‌گیرم. بعد از گرفتن این exception نوبت به Mock.Assert می‌رسد. اگر این کار کند، باید به اجرا کننده تست بگویم که تست pass شده است پس از Assert.Pass استفاده می‌کنم. اگر نوع دیگری از exception ایجاد شد یا کد خطا نداد Assert.Fail باعث fail شدن تست می‌شود.

تست را اجرا می‌کنیم تا ببینیم fail می‌شود یا خیر؟

این fail به این خاطر است که setup مربوط به stub ما انتظار دارد که متد Save ذقیقاً یک بار اجرا شود. این را عمداً اصلاح نکردم تا fail شدن تست را ببینید. حالا کد را تغییر می‌دهیم تا مطمئن شویم متد Save هرگز فراخوانی نمی‌شود:

[Test]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
	var customerId = Guid.NewGuid();
	var expectedOrderId = Guid.NewGuid();

	Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
		.Returns(expectedOrderId)
		.OccursNever();

	//Act
	try
	{
		_orderService.PlaceOrder(customerId, shoppingCart);
	}
	catch(InvalidOrderException ex)
	{
		//Assert
		Mock.Assert(_orderDataService);
		Assert.Pass();
	}

	//Assert
	Assert.Fail();
}

تست را دوباره اجرا می‌کنیم و می‌بینیم تست pass می‌شود.

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

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز چهاردهم - ساده همیشه به معنی واضح نیست قسمت اول

داستان ۳۰ روز با TDD

سپتامبر سال گذشته آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما چهاردهمین روز: ساده همیشه به معنی واضح نیست

در نوشته‌های قبلی دیدیم که چطور با استفاده از stub ها کد کلاسی وابسته زیر تست را mock کردیم. نوشته انگلیسی روز چهاردهم را در این آدرس می‌توانید مطالعه کنید.

یک روز دیگر، یک تست دیگر

برای این نوشته نیز مثال فروشگاه مطالب قبلی را ادامه می‌دهیم. تست بعدی زمانی است که کاربر یک آیتم را سفارش می‌دهد و تعداد سفارش صفر است و InvalidOrderException باید throw شود.

در ظاهر این یک تست ساده به نظر می‌رسد: هر سفارش لیستی از آیتم‌ها دارد اگر تعداد هر یک از آیتم‌ها برابر صفر بود یک exception را throw‌ کن. در اغلب موارد انتظار داریم همراه exception یک پیغام هم برگردد که مشخص کند مشکل کجا بوده است، اما حالا برای ساده نگه داشتن مثال این best practice را نادیده می‌گیریم (در نوشته بعدی به آن خواهیم رسید) به نظر ساده می‌آید، این طور نیست؟ اجازه بدهید تست را بنویسیم

[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange

	//Act

	//Assert
}

کد بالا ساده‌ترین حالت ممکن است. اگر سری نوشته‌های ۳۰ روز با TDD را از ابتدا دنبال کرده‌اید، چیز جدیدی اینجا نیست. تست خود را با ویژگی Test و همچنین ExpectedExpection معرفی می‌کنیم که به NUnit می‌گوید که انتظار یک exception از نوع InvalidOrderException داشته باشد. تعدادی کامنت هم قرار دادیم که یادآور الگوی AAA باشد و بخش‌های Arrange و Act‌ و Assert را مشخص کند. اغلب توسعه‌دهندگان TDD از بخش Arrange شروع می‌کنند که مشکلی هم برای شروع از این قسمت نیست. شخصاً تمایل دارم از بخش Assert شروع کرده و به عقب بروم. من دوست دارم ابتدا assert را بنویسم که می‌توانم از کلاس تست مشتقش کنم. با داشتن assert از ابتدا می‌دانم که چه چیزی را در بخش Act باید فراخوانی کنم تا نتیجه مورد انتظار تست را داشته باشم و با نوشتن Act در مرحله دوم می‌توانم مطمئن شوم که در قسمت Arrange تنها متغیرها، mock و stub هایی که برای تست در بخش Act نیاز است را تنظیم می‌کنم.

برای این تست یک stub برای OrderDataService نیاز دارم. از آنجایی که انتظار دارم در این تست یک exception ایجاد شود و نه اینکه یک سفارش ثبت شود پس توقع دارم که متد Save اصلاً صدا زده نشود (چون در صورت فراخوانی دردسر بزرگی خواهم داشت!)


[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange

	//Act

	//Assert
	Mock.Assert(orderDataService);
}

تا اینجا همه چیز خوب است، حالا باید قسمت Act را بنویسیم

[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange

	//Act
	orderService.PlaceOrder(customerId, shoppingCart);

	//Assert
	Mock.Assert(orderDataService);
}

در مثال قبلی، من مقدار متد PlaceOrder را گرفتم و در بخش Assert استفاده کردم. در این مثال یک exception انتظار دارم، پس نتیجه بیهوده است. حالا نوبت به قسمت Arrange می‌رسد. با نگاه به کد اولین چیزی که لازم دارم مشخص می‌شود یک وهله (instance) از OrderService است. تست دیگرم نیز از OrderService استفاده می‌کنم. من مایلم تست‌هایم را تا حد ممکن DRY یا Don't Repeat Yourself نگه دارم و دلیلی نیست که گام‌های ایجاد این instance مانند مطلب قبلی به یک setup method منتقل نشود

[TestFixture]
class OrderServiceTests
{
	private OrderService _orderService;

	[TestFixtureSetUp]
	public void SetupTestFixture()
	{
		_orderService = new OrderService();
	}

به نیمه راه رسیدیم. اگر به یاد داشته باشید OrderService یک وابستگی به OrderDataService دارد که باید برایش یک mock ایجاد کنم. خوشبختانه می‌توانم mock را به عنوان یک متغیر تعریف کرده و به سازنده (constructor) مربوط به OrderService در TestFixtureSetup تزریق کنم.


[TestFixture]
class OrderServiceTests
{
	private OrderService _orderService;
	private IOrderDataService _orderDataService;

	[TestFixtureSetUp]
	public void SetupTestFixture()
	{
		_orderDataService = Mock.Create();
		_orderService = new OrderService(_orderDataService);
	}

وقتی این کار انجام شد می‌توانم کد موجود را برای استفاده از این متغیرها به جای local instance ها refactor کنم.

[Test]
public void WhenUserPlacesACorrectOrderThenAnOrderNumberShouldBeReturned()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
	var customerId = Guid.NewGuid();
	var expectedOrderId = Guid.NewGuid();

	Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
		.Returns(expectedOrderId)
		.OccursOnce();
	
	//Act
	var result = _orderService.PlaceOrder(customerId, shoppingCart);

	//Assert
	Assert.AreEqual(expectedOrderId, result);
	Mock.Assert(_orderDataService);
}

همانطور که مشاهده می‌کنید خطوط مربوط به ایجاد mock های OrderDataService و OrderService ‌را حذف کردم. اما بعد از refactor برای استفاده از متغیرهای جدید کد من خطای کامپایلی دریافت می‌کند.

ما به InvalidOrderException تا حالا نیاز نداشتیم. اما حالا که تستی داریم که exception ای از این نوع ایجاد می‌کند باید یکی بسازیم.

من دوست دارم که پروژه را به شدت مرتب نگه دارم. به عنوان بخشی از این نگاه، exception های سفارشی را در پوشه مخصوصی در پروژه نگه می‌دارم که باعث می‌شود در فضا نام (namespace) مربوط به خود هم قرار بگیرند. در پروژه TddStore.Core پوشه‌ای به نام Exceptions ایجاد می‌کنم. یک کلاس جدید به نام InvalidOrderException در این پوشه می‌سازم و فعلاً پیاده‌سازی‌ام برای این کلاس به سادگی کد زیر خواهد بود:


[Test]
using System;
 
namespace TddStore.Core.Exceptions
{
    public class InvalidOrderException : Exception
    {
        
    }
}

تنها لازم است از using برای اشاره به فضا نام TddStore.Core.Exceptions استفاده کنم. بعد از انجام این کار تست اصلی پاس می‌شود و بنابراین می‌توانم به تست جدید برگردم. اول کمی refactor برای کدهای موجود نیاز داریم سپس لازم است روی بخش Arrange کار کنیم. ابتدا باید متغیرها را برای فراخوانی PlaceOrder تنظیم کنیم پس من customerId و shoppingCart را تعریف می‌کنم. در حالی که روی این موضوع کار می‌کنم آیتمی را به shoppingCart با مقدار صفر اضافه می‌کنم

[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
	var customerId = Guid.NewGuid();
	var expectedOrderId = Guid.NewGuid();

	Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
		.Returns(expectedOrderId)
		.OccursOnce();

	//Act
	_orderService.PlaceOrder(customerId, shoppingCart);

	//Assert
	Mock.Assert(_orderDataService);
}

سپس لازم است تا mock را آماده کنم

[Test]
[ExpectedException(typeof(InvalidOrderException))]
public void WhenAUserAttemptsToOrderAnItemWithAQuantityOfZeroThrowInvalidOrderException()
{
	//Arrange
	var shoppingCart = new ShoppingCart();
	shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 0 });
	var customerId = Guid.NewGuid();
	var expectedOrderId = Guid.NewGuid();

	Mock.Arrange(() => _orderDataService.Save(Arg.IsAny()))
		.Returns(expectedOrderId)
		.OccursNever();

	//Act
	_orderService.PlaceOrder(customerId, shoppingCart);

	//Assert
	Mock.Assert(_orderDataService);
}

بر خلاف تست قبلی می‌خواهم مطمئن شوم که متد Save در OrderDataService فراخوانی نمی‌شود. همانطور که در نوشته قبلی این سری نوشته‌ها دیدید Just Mock‌ (محصولی از شرکت Telerik) امکان اجرای یک بار stub ای که exception ایجاد می‌کند را می‌دهد. بنابراین به صورت مشابه از متد OccursNever استفاده می‌کنم که بدان معنی است که این متد در حوزه تست فراخوانی نشود.

اجرای تست به دلیل پیاده‌سازی نشدن منطق برنامه با شکست مواجه می‌شود. بر اساس ایده اجرای ساده‌ترین راه حل ممکن، کد زیر را خواهیم داشت.

public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
{
	foreach (var item in shoppingCart.Items)
	{
		if (item.Quantity == 0)
		{
			throw new InvalidOrderException();
		}
	}
	var order = new Order();
	return _orderDataService.Save(order);
}

با اجرای تست‌ها می‌بینم که هر دو تست Pass می‌شوند. 

کار من تمام شد!

آیا کار تمام شده است؟

در نوشته بعدی خواهیم دید که یک اشکال در تست‌ها به ما حس اطمینان اشتباهی می‌دهد.

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

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

۳۰ روز با TDD: روز سیزدهم - ویژگی‌های بیشتر stub

داستان ۳۰ روز با TDD

سپتامبر سال گذشته آقای James Bender در وبلاگ‌های تلریک یک مجموعه نوشته منتشر کرد به نام ۳۰ روز با TDD. در ترجمه‌ای آزاد در وبلاگ آرایه، با هم در طول یک ماه با Test Driven Development آشنا می‌شویم. لینک سایر نوشته‌های این سری را در صفحه ۳۰ روز با TDD می‌توانید مشاهده کنید.

و اما سیزدهمین روز: ویژگی‌های بیشتر stub

در روز دوازدهم با stub ها آشنا شدیم. نوشته روز سیزدهم به زبان انگلیسی را در این آدرس می‌توانید مطالعه کنید. 

زمان بازبینی کد

من دوست دارم به صورت متناوب با تیم برای بازبینی کدها جلسه داشته باشم. بعضی تیم‌ها زمان‌های برنامه‌ریزی شده برای این کار دارند و بعضی ندارند. یکی از چیزهایی که دوست دارم پایه اصلی جلسات بازبینی کد باشد، بخش‌های مهم برنامه هستند که یا پیجیده‌اند یا بخش‌های زیاد دیگری با آن‌ها در ارتباطند. بازبینی کد این بخش‌ها کمک می‌کند مطمئن شویم که مشکلات پیچیده به ساده‌ترین شکل ممکن هستند و همه تیم (حتی شده به صورت کلی) بدانند آن بخش‌های پیچیده و مهم چطور کار می‌کنند. 

زمان دیگری که وقت بازبینی کد است موقع ورود یک برنامه‌نویس جدید به تیم است. این برنامه‌نویس جدید ممکن است یک برنامه‌نویس تازه‌کار که اخیراً از دانشگاه آمده باشد یا خیلی ساده یک نفر که به تازگی وارد تیم شما شده است. من دوست دارم که چند task اولی که انجام دادند را بازبینی کنم تا مطمئن شوم درک درستی از مساله دارند و با بخش‌هایی از برنامه که تا به حال نوشته شده آشنا هستند و از استانداردهای کدنویسی ما معطلند. از آنجایی که خیلی از خوانندگان این سری نوشته در TDD تازه‌وارد هستند، به نظر می‌رسد زمان مناسبی برای بازبینی کدها باشد!

در مطلب قبلی ما کد زیر را برای متد PlaceOrder در کلاس OrderService  نوشتیم:

public Guid PlaceOrder(Guid customerId, ShoppingCart shoppingCart)
        {
            var order = new Order();
            return _orderDataService.Save(order);
        }

کد خیلی ساده‌ای است. برای اینکه تایید کنیم مطابق نیاز تجاری است یک تست داریم (که پیش از کد بالا ایجاد شده است)


[Test]
        public void WhenUserPlacesACorrectOrderThenAnOrderNumberShouldBeReturned()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
            var customerId = Guid.NewGuid();
            var expectedOrderId = Guid.NewGuid();
 
            var orderDataService = Mock.Create();
            Mock.Arrange(() => orderDataService.Save(Arg.IsAny())).Returns(expectedOrderId);
            OrderService orderService = new OrderService(orderDataService);
 
            //Act
            var result = orderService.PlaceOrder(customerId, shoppingCart);
 
            //Assert
            Assert.AreEqual(expectedOrderId, result);
        }

تست ما برای تایید صحت کار مورد نیاز خوب به نظر می‌رسد. اما به عنوان یک معمار، یک نکته قابل تامل می‌بینم. در طی سالیان با برنامه‌های متعددی کار کردم و مشکلات کارآیی (performance) گاه و بیگاه را دیده‌ام. دلایل متعددی برای مشکلات performance وجود دارد، اما مشکل مشترکی که من دیدم، فراخوانی‌های متعدد غیرضروری به منابع خارجی مانند دیتابیس بوده است. فراخوانی دیتابیس به صورت نسبی کند است، به خصوص به نسبت زمانی که از حافظه یا mock استفاده می‌کنید. این باعث می‌شود که پیدا کردن این طور مشکلات از طریق آزمون واحد مشکل باشد و البته هیچ جریمه‌ مرتبط با performance ای هم در کار نیست چرا که از mock استفاده می‌کنیم. به عنوان معمار/برنامه‌نویس ارشد/هر عنوان دیگر پروژه می‌خواهم مطمئن شوم که Save در OrderDataService یک بار فراخوانی می‌شو. خوشبختانه JustMock امکانی برای این منظور دارد که stub چند بار فراخوانی شود.


[Test]
        public void WhenUserPlacesACorrectOrderThenAnOrderNumberShouldBeReturned()
        {
            //Arrange
            var shoppingCart = new ShoppingCart();
            shoppingCart.Items.Add(new ShoppingCartItem { ItemId = Guid.NewGuid(), Quantity = 1 });
            var customerId = Guid.NewGuid();
            var expectedOrderId = Guid.NewGuid();
 
            var orderDataService = Mock.Create();
            Mock.Arrange(() => orderDataService.Save(Arg.IsAny()))
                .Returns(expectedOrderId)
                .OccursOnce();
            OrderService orderService = new OrderService(orderDataService);
 
            //Act
            var result = orderService.PlaceOrder(customerId, shoppingCart);
 
            //Assert
            Assert.AreEqual(expectedOrderId, result);
            Mock.Assert(orderDataService);
        }

چند تغییر در تست اولیه ایجاد کردم. اول، بخش Arrangement در mock را به سه خط تقسیم کردم تا خواندن این مثال راحت‌تر باشد. همچنین متد OccursOnce را به انتهای arrangement اضافه کردم که به JustMock می‌گوید این متد دقیقاً یک بار باید اجرا شود و هر تعداد دیگر اجرا یک خطا خواهد بود. همچنین فراخوانی Mock.Assert را اضافه کردم. این فراخوانی به mock می‌گوید که قوانین تعریف شده (مثل اجرای یک بار متد) را تایید کند. در این مثال ما تنها یک قانون داریم که متد Save باید دقیقاً یک بار فراخوانی شود. اگر متد Save بیش از یک بار فراخوانی شود تست ما fail خواهد شد.

برای نشان دادن fail شدن من فراخوانی متد Save را در PlaceOrder کامنت می‌کنم و همچنین Assert.Equal را نیز کامنت می‌کنم تا مطئن شوم که تایید قوانین mock اولین fail است که پیدا می‌شود. وقتی تست را اجرا می‌کنم تست fail می‌شود و دلیل fail شدن یکسان نبودن تعداد دفعات فراخوانی با تعداد دفعات فراخوانی مورد انتظار است.

JustMock انتظار دارد که متد یک بار فراخوانی شود (محدوده مورد انتظار بین ۱ بار و ۱بار است) و همانطور که در نوشته‌های بعدی خواهید دید حد پایین و بالای تعداد فراخوانی‌ها را می‌توانیم تنظیم کنیم. چون از OccursOnce استفاده کردیم محدوده ما بین یک و یک است.

بعد از برداشتن کامنت‌ها اگر دوباره تست را اجرا کنیم تست pass خواهد شد.

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

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی