2016-05-22 11 views
-1

私は単体テストをよりよくするために努力しています。私の最大の不確実性の1つは、かなりのセットアップコードが必要なメソッドの単体テストを書くことです。 。私が見つけた答えは、「テストをより小さな単位に分割する」または「モックを使用する」という行に沿っています。私はそれらのベストプラクティスに従っています。しかし、私は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); 
    } 
} 

これは略す例です。私が実際にテストしているメソッドは構造が似ていますが、呼び出しているサービスメソッドがいくつかある場合や、モデルのセットアップコードが多い場合があります(たとえば、Orderオブジェクトには、テストが機能するように)。

私のメソッドのいくつかは、この例のように(ボタンクリックイベントの背後にあるメソッドなど)すぐにいくつかのことを行う必要があることを考えれば、これらのメソッドの単体テストを扱う最良の方法でしょうか?

答えて

1

あなたはすでに適切なパスにいます。そして、ある時点で、「テスト中のメソッド」が大きければ(複雑ではない)、単体テストは大きく(複雑ではない)拘束されます。私は '大きい'コードと '複雑なコード'を区別する傾向があります。複雑なコードスニペットを単純化する必要があります。ビッグコードスニペットは時にはより明瞭で簡単です。

あなたのコードはちょうど大きく、複雑ではありません。したがって、単体テストが大きければ大きな問題ではありません。

ここで、私たちはそれをより鮮明でより読みやすくすることができます。

オプション#1

テスト対象ターゲットコードがあると思われる:

パブリックブール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); 
    } 

ご覧のとおり、このテストはかなりコンパクトです。あなたのシナリオコードがヒットするとは思わない、たくさんのモックなどをセットアップするのは面倒ではありません。

同様に、あなたは...だけでなく

を他の2つのシナリオのためのコンパクトなテストを書くことができ、その後、最終的には最後のシナリオのために、あなたは適切なユニットテストを持っています。私はどうしたら 唯一のことは、次のように実際のユニットテストはかなりくっきりと読みやすくするために、いくつかのプライベートヘルパーメソッドを抽出です:

[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); 
    } 

あなたが見ることができるように、ヘルパーメソッドを使用することにより、あなたはテストを小さくし、 crisperですが、どのパラメータが設定されているかという点では読みやすさが失われます。

しかし、私はヘルパーメソッド名とパラメータ名について非常に明示的な傾向があるので、メソッド名とパラメータを読むことで、どのような種類のデータが配置されているかがわかります。

ほとんどの場合、ハッピーパスのシナリオは、関連するすべてのアイテム、数量、価格などですべての模擬設定を正しく設定する必要があるため、最大セットアップコードが必要になります。 TestSetupメソッドのセットアップコード。すべてのテストメソッドでデフォルトで使用できるようにします。

これは、TestSetupメソッドの中で有効なOrderを準備しておくことができるので、

ハッピーパスのシナリオは通常1つの単体テストですが、testSetupにそのようなものを置くことは、それが決して必要ではないとしても、すべての単体テストに対して実行します。

ここでオプション#2

は別の方法..です

あなたは可能性があり、それぞれが4つのシナリオを行使する4つのプライベートメソッドへの分解、あなたのIsOrderShippable方法を。これらのプライベートメソッドを内部で作成し、単体テストを行い、それらのメソッド(internalsvisibletoなど)に取り組むことができます。プライベートメソッドを内部で作成しているので、私たちは元の問題にちょっと戻ってきます。

+0

お返事ありがとうございます。私はいくつかのコード行以上を必要とするテストのような例を見つけるのが難しかった。 –

関連する問題