使用复杂设置和逻辑进行单元测试

时间:2016-05-22 23:46:44

标签: c# unit-testing moq

我试图在单元测试方面做得更好,而我最大的不确定因素之一是为需要相当多设置代码的方法编写单元测试,但我还没有找到一个好的答案。我找到的答案一般都是"将你的测试分解成更小的工作单元"或者"使用模拟"。我试图遵循所有这些最佳做法。然而,即使使用模拟(我使用Moq)并尝试将所有内容分解为最小的工作单元,我最终会遇到一个具有多个输入的方法,调用多个模拟服务,并要求我指定返回那些模拟方法调用的值。

以下是受测试代码的示例:

public class Order
{
   public string CustomerId { get; set; }
   public string OrderNumber { get; set; }
   public List<OrderLine> Lines { get; set; }
   public decimal Value { get { /* return the order's calculated value */ } }

   public Order()
   {
      this.Lines = new List<OrderLine>();
   }
}

public class OrderLine
{
   public string ItemId { get; set; }
   public int QuantityOrdered { get; set; }
   public decimal UnitPrice { get; set; }
}

public class OrderManager
{
   private ICustomerService customerService;
   private IInventoryService inventoryService;

   public OrderManager(ICustomerService customerService, IInventoryService inventoryService)
   {
      // Guard clauses omitted to make example smaller
      this.customerService = customerService;
      this.inventoryService = inventoryService;
   }

   // This is the method being tested.  
   // Return false if this order's value is greater than the customer's credit limit.
   // Return false if there is insufficient inventory for any of the items on the order.
   // Return false if any of the items on the order on hold.
   public bool IsOrderShippable(Order order)
   {
      // Return false if the order's value is greater than the customer's credit limit
      decimal creditLimit = this.customerService.GetCreditLimit(order.CustomerId);
      if (creditLimit < order.Value)
      {
         return false;
      }

      // Return false if there is insufficient inventory for any of this order's items
      foreach (OrderLine orderLine in order.Lines)
      {
         if (orderLine.QuantityOrdered > this.inventoryService.GetInventoryQuantity(orderLine.ItemId)
         {
            return false;
         }
      }

      // Return false if any of the items on this order are on hold
      foreach (OrderLine orderLine in order.Lines)
      {
         if (this.inventoryService.IsItemOnHold(orderLine.ItemId))
         {
            return false;
         }
      }

      // If we are here, then the order is shippable
      return true;
   }
}

这是一个测试:

[TestClass]
public class OrderManagerTests
{
   [TestMethod]
   public void IsOrderShippable_OrderIsShippable_ShouldReturnTrue()
   {
      // Setup inventory on-hand quantities for this test
      Mock<IInventoryService> inventoryService = new Mock<IInventoryService>();
      inventoryService.Setup(e => e.GetInventoryQuantity("ITEM-1")).Returns(10);
      inventoryService.Setup(e => e.GetInventoryQuantity("ITEM-2")).Returns(20);
      inventoryService.Setup(e => e.GetInventoryQuantity("ITEM-3")).Returns(30);

      // Configure each item to be not on hold
      inventoryService.Setup(e => e.IsItemOnHold("ITEM-1")).Returns(false);
      inventoryService.Setup(e => e.IsItemOnHold("ITEM-2")).Returns(false);
      inventoryService.Setup(e => e.IsItemOnHold("ITEM-3")).Returns(false);

      // Setup the customer's credit limit
      Mock<ICustomerService> customerService = new Mock<ICustomerService>();
      customerService.Setup(e => e.GetCreditLimit("CUSTOMER-1")).Returns(1000m);

      // Create the order being tested
      Order order = new Order { CustomerId = "CUSTOMER-1" };
      order.Lines.Add(new OrderLine { ItemId = "ITEM-1", QuantityOrdered = 10, UnitPrice = 1.00m });
      order.Lines.Add(new OrderLine { ItemId = "ITEM-2", QuantityOrdered = 20, UnitPrice = 2.00m });
      order.Lines.Add(new OrderLine { ItemId = "ITEM-3", QuantityOrdered = 30, UnitPrice = 3.00m });

      OrderManager orderManager = new OrderManager(
         customerService: customerService.Object,
         inventoryService: inventoryService.Object);
      bool isShippable = orderManager.IsOrderShippable(order);

      Assert.IsTrue(isShippable);
   }
}

这是一个简短的例子。我测试的实际方法在结构上是相似的,但是它们通常还有一些他们正在调用的服务方法,或者他们有更多的模型设置代码(例如,{{1} } object需要分配更多属性才能使测试工作。)

鉴于我的一些方法必须像这个例子一样做一些事情(比如按钮点击事件背后的方法),这是处理这些方法的单元测试的最佳方法吗?

1 个答案:

答案 0 :(得分:1)

你已经走在了正确的道路上。而且在某些时候,如果“测试中的方法”很大(不复杂),那么你的单元测试肯定会很大(不复杂)。我倾向于区分“大”代码和“复杂”代码。一个复杂的代码片段需要简化..一个大的代码片段有时更清晰而简单..

在您的情况下,您的代码很大,而不是很复杂。因此,如果您的单元测试也很大,那么这不是什么大问题。

话虽如此,我们仍然可以使它更加清晰,更具可读性。

选项#1

正在测试的目标代码似乎是:

public bool IsOrderShippable(订单)

正如我所看到的,至少有4个单元测试场景:

   // Scenario 1: Return false if the order's value is 
   // greater than the customer's credit limit

   [TestMethod]
   public void IsOrderShippable_OrderValueGreaterThanCustomerCreditLimit_ShouldReturnFalse()
   {
      // Setup the customer's credit limit
      var customerService = new Mock<ICustomerService>();
      customerService.Setup(e => e.GetCreditLimit(It.IsAny<string>())).Returns(1000m);

      // Create the order with value greater than credit limit
      var order = new Order { Value = 1001m };

      var orderManager = new OrderManager(
         customerService: customerService.Object,
         inventoryService: new Mock<IInventoryService>().Object);

      bool isShippable = orderManager.IsOrderShippable(order);

      Assert.IsFalse(isShippable);
   }

如您所见,此测试非常紧凑。它不会设置很多你不希望你的场景代码命中的模拟等。

同样你也可以为其他两个场景编写紧凑的测试。

然后最后在最后一个场景中,你有适当的单元测试。 我唯一要做的就是提取一些私有的帮助器方法,使实际的单元测试非常清晰可读,如下所示:

   [TestMethod]
   public void IsOrderShippable_OrderIsShippable_ShouldReturnTrue()
   {
      // you can parametrize this helper method as needed
      var inventoryService = GetMockInventoryServiceWithItemsNotOnHold();

      // You can parametrize this helper method with credit line, etc.
      var customerService = GetMockCustomerService(1000m);

      // parametrize this method with number of items and total price etc.
      Order order = GetTestOrderWithItems();

      OrderManager orderManager = new OrderManager(
         customerService: customerService.Object,
         inventoryService: inventoryService.Object);

      bool isShippable = orderManager.IsOrderShippable(order);

      Assert.IsTrue(isShippable);
   }

正如您所看到的,通过使用辅助方法,您可以使测试更小更清晰,但我们确实在设置参数方面失去了一些可读性。

但是,我倾向于非常清楚地了解辅助方法名称和参数名称,因此通过阅读方法名称和参数,读者可以清楚地了解正在排列的数据类型。

大多数情况下,快乐路径场景最终需要最大的设置代码,因为它们需要所有模拟设置正确的所有相关项目,数量,价格等。在这些情况下,我更喜欢有时放置所有设置TestSetup方法上的代码..因此默认情况下它可用于每个测试方法。

好处是,测试得到了一个很好的模拟值开箱即用。(你的快乐路径单元测试可以只是2行,因为你可以在TestSetup方法中保持一个良好有效的Order)< / p>

缺点是快乐路径场景通常是一个单元测试..但是将这些东西放在testSetup中会为每个单元测试运行它,即使它们永远不需要它。

选项#2

这是另一种方式..

您可以将IsOrderShippable方法分解为4个私有方法,每个方法执行4个方案。您可以将这些私有方法设置为内部,然后进行单元测试,对这些方法进行处理(内部可见等)..它仍然有点笨拙,因为您正在内部使用私有方法,而且您还需要对公共单元进行单元测试方法,它让我们回到原来的问题。