۳۰ روز با 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 خواهد شد.
ادامه دارد ...
به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

نظرات شما درباره این نوشته

نظرات پس از بررسی و تایید منتشر خواهند شد.

  1. سلام ممنون از مطلب مفیدتون