0

我有一個關於設計類以便測試友好的最佳方式的問題。假設我有一個OrderService類,用於放置新訂單,檢查訂單狀態等。該類將需要訪問客戶信息,庫存信息,運輸信息等。因此,OrderService類將需要使用CustomerService,InventoryService和ShippingService。每個服務還有其自己的後備庫。測試友好架構

什麼是設計OrderService類可以輕鬆測試的最佳方式?我見過的兩種常用模式是依賴注入和服務定位器。對於依賴注入,我會做這樣的事情:

class OrderService 
{ 
    private ICustomerService CustomerService { get; set; } 
    private IInventoryService InventoryService { get; set; } 
    private IShippingService ShippingService { get; set; } 
    private IOrderRepository Repository { get; set; } 

    // Normal constructor 
    public OrderService() 
    { 
     this.CustomerService = new CustomerService(); 
     this.InventoryService = new InventoryService(); 
     this.ShippingService = new ShippingService(); 
     this.Repository = new OrderRepository();   
    } 

    // Constructor used for testing 
    public OrderService(
     ICustomerService customerService, 
     IInventoryService inventoryService, 
     IShippingService shippingService, 
     IOrderRepository repository) 
    { 
     this.CustomerService = customerService; 
     this.InventoryService = inventoryService; 
     this.ShippingService = shippingService; 
     this.Repository = repository; 
    } 
} 

// Within my unit test 
[TestMethod] 
public void TestSomething() 
{ 
    OrderService orderService = new OrderService(
     new FakeCustomerService(), 
     new FakeInventoryService(), 
     new FakeShippingService(), 
     new FakeOrderRepository()); 
} 

這個缺點是,每次我創造,我使用的測試中OrderService對象時,它需要大量的代碼來調用我的測試中的構造函數。我的服務類也最終爲他們使用的每個服務和存儲庫類提供了一堆屬性。隨着我擴展我的程序並在各種Service和Repository類之間添加更多依賴關係,我必須返回併爲我已經創建的類的構造函數添加越來越多的參數。

對於一個服務定位器模式,我可以做這樣的事情:

class OrderService 
{ 
    private CustomerService CustomerService { get; set; } 
    private InventoryService InventoryService { get; set; } 
    private ShippingService ShippingService { get; set; } 
    private OrderRepository Repository { get; set; } 

    // Normal constructor 
    public OrderService() 
    { 
     ServiceLocator serviceLocator = new ServiceLocator(); 
     this.CustomerService = serviceLocator.CreateCustomerService() 
     this.InventoryService = serviceLocator.CreateInventoryService(); 
     this.ShippingService = serviceLocator.CreateShippingService(); 
     this.Repository = serviceLocator.CreateOrderRepository();   
    } 

    // Constructor used for testing 
    public OrderService(IServiceLocator serviceLocator) 
    { 
     this.CustomerService = serviceLocator.CreateCustomerService() 
     this.InventoryService = serviceLocator.CreateInventoryService(); 
     this.ShippingService = serviceLocator.CreateShippingService(); 
     this.Repository = serviceLocator.CreateOrderRepository(); 
    } 
} 

// Within a unit test 
[TestMethod] 
public void TestSomething() 
{ 
    OrderService orderService = new OrderService(new TestServiceLocator()); 
} 

我怎麼樣在更少的代碼服務定位器模式的結果調用構造函數的時候,但它也給缺乏靈活性。

什麼是建立我的服務類與其他幾個服務和存儲庫的依賴關係,以便他們可以很容易地測試?我所展示的方式之一還是兩者都好,還是有更好的方法?

+0

只要在第二個例子中刪除「正常的構造函數」,你就很好。但你可能想重新考慮單一責任原則。爲什麼這個類需要這麼多的依賴關係? – CSharpie

+0

研究依賴注入和IOC框架,然後你可以模擬依賴關係,並專注於測試類的核心功能。 –

+0

CSharpie - 它需要所有這些依賴性,因爲創建新訂單涉及檢查客戶信用,檢查庫存,檢查運輸時間等。讓代碼在OrderService類中完成所有這些操作會使其變得非常龐大,所以我有其他的Service類來完成這些任務。 –

回答

1

有代碼之間的差異是「可驗證」和代碼是鬆散耦合的。

使用DI的主要目的是鬆耦合。可測試性是從鬆耦合代碼獲得的副作用。但是可測試的代碼不一定是鬆散耦合的。

雖然注入服務定位器顯然比靜態引用更鬆散耦合,但它仍然不是最佳實踐。最大的缺點是lack of transparency of dependencies。你現在可以通過實現一個服務定位器保存幾行代碼,然後認爲你贏了,但是當你真的需要編寫你的應用程序的時候,所獲得的任何東西都會丟失。在intellisense中查看構造函數有一個明顯的優點,那就是確定一個類具有哪些依賴關係,然後定位該類的源代碼以嘗試找出它的依賴關係。

因此,正如您可能已經猜到的那樣,我建議您使用構造函數注入。但是,在您的示例中,您也有一種稱爲bastard injection的反模式。混蛋注射的主要缺點是你要通過在內部新建它們來將你的課程彼此緊密聯繫起來。這可能看起來很無辜,但如果您需要將服務移動到單獨的庫中,會發生什麼?這很可能會導致應用程序中的循環依賴。

處理此問題的最佳方法(特別是在處理服務而不是配置設置時)是使用pure DI或DI容器,並且只有一個構造函數。您還應該使用警戒條款,以確保沒有任何方法可以在沒有任何依賴關係的情況下創建訂單服務。

class OrderService 
{ 
    private readonly ICustomerService customerService; 
    private readonly IInventoryService inventoryService; 
    private readonly IShippingService shippingService; 
    private readonly IOrderRepository repository; 


    // Constructor used for injection (the one and only) 
    public OrderService(
     ICustomerService customerService, 
     IInventoryService inventoryService, 
     IShippingService shippingService, 
     IOrderRepository repository) 
    { 
     if (customerService == null) 
      throw new ArgumentNullException("customerService"); 
     if (inventoryService == null) 
      throw new ArgumentNullException("inventoryService"); 
     if (shippingService == null) 
      throw new ArgumentNullException("shippingService"); 
     if (repository == null) 
      throw new ArgumentNullException("repository");    

     this.customerService = customerService; 
     this.inventoryService = inventoryService; 
     this.shippingService = shippingService; 
     this.repository = repository; 
    } 
} 

// Within your unit test 
[TestMethod] 
public void TestSomething() 
{ 
    OrderService orderService = new OrderService(
     new FakeCustomerService(), 
     new FakeInventoryService(), 
     new FakeShippingService(), 
     new FakeOrderRepository()); 
} 

// Within your application (pure DI) 
public class OrderServiceContainer 
{ 
    public OrderServiceContainer() 
    { 
     // NOTE: These classes may have dependencies which you need to set here. 
     this.customerService = new CustomerService(); 
     this.inventoryService = new InventoryService(); 
     this.shippingService = new ShippingService(); 
     this.orderRepository = new OrderRepository(); 
    } 

    private readonly IOrderService orderService; 
    private readonly ICustomerService customerService; 
    private readonly IInventoryServcie inventoryService; 
    private readonly IShippingService shippingService; 
    private readonly IOrderRepository orderRepository; 

    public ResolveOrderService() 
    { 
     return new OrderService(
      this.customerService, 
      this.inventoryService, 
      this.shippingService, 
      this.orderRepository); 
    } 
} 

// In your application's composition root, resolve the object graph 
var orderService = new OrderServiceContainer().ResolveOrderService(); 

我也同意戈登的回答。如果你有4個服務依賴關係,這是一種代碼味道,你的班級承擔了太多的責任。你應該考慮對aggregate services進行重構,以使你的課程單獨承擔責任。當然,有時需要4個依賴關係,但總是值得回顧一下是否有一個領域概念應該是另一個明確的服務。

注意:我不一定說Pure DI是最好的方法,但它可以用於一些小型應用程序。當應用程序變得複雜時,使用DI容器可以通過使用基於約定的配置來分紅。

+0

我同意在一個類中有多個服務依賴關係很多,但是當用戶單擊一個按鈕時,總會有很多事情需要發生。因爲我試圖遵守單一職責,所以點擊按鈕後面的事件需要使用幾種不同的服務來處理所有事情。我想測試點擊按鈕後面的整個程序流程,包括測試整個流程中的任何條件,這些條件可能不會從單獨測試每個服務時明顯看出。如果沒有所有這些依賴關係,我該如何實現? –

+0

首先,請閱讀我在我的回答中鏈接到的文章。給出的示例與問題域非常相似。這是一個[很好的例子,不要做](http://nopcommerce.codeplex.com/SourceControl/latest#src/Presentation/Nop.Web/Controllers/ProductController.cs) - 這個控制器有最糟糕的構造函數 - 我見過的注射。有明顯的改善空間。例如,我會查看稅務服務,貨幣服務,價格計算服務和價格格式化程序是否在同一地點使用,如果是,則使用一個服務或兩個服務。 – NightOwl888

7

只是一個非常快速的答案,讓你走上正軌。根據我的經驗,如果您的目標是輕鬆測試代碼,那麼您最終會得到乾淨的可維護代碼,這是一個不錯的副作用。 :-)

一些關鍵點要記住:

  1. 固體原則將真正幫助你創造良好的,乾淨的,可測試的代碼。

    (S + O + I)將此服務分成只能做一件事的小型服務,因此只有一個理由需要更改。至少下訂單並檢查訂單的狀態是完全不同的事情。如果你深入思考,你並不需要遵循最明顯的步驟(例如,檢查信用 - >檢查庫存 - >檢查運輸),其中一些可以不按順序完成 - 但這是一個完整的故事可能需要不同的商業模式。無論如何,如果您真的需要,可以使用Facade模式在這些小型服務之上創建一個簡化的視圖。

  2. 使用IoC容器(如統一)

  3. 用嘲弄的框架(如MOQ)

  4. Service Locator模式實際上被認爲是一個反模式/代碼味道 - 所以請不要拿不要用它。

  5. 你的測試應該使用與你真實代碼相同的路徑,所以擺脫'普通構造函數'。在你的第一個例子中,'用於測試的構造函數'是你的構造函數應該是什麼樣的。
  6. 不要在你的類中實例化所需的服務 - 它們應該作爲接口傳入。 IoC容器將幫助您處理這部分內容。通過這樣做,您可以遵循固體中的D(依賴性反轉原理)
  7. 儘可能避免直接在自己的類中使用/引用靜態類/方法。這裏我正在討論直接使用諸如DateTime.Now()之類的東西,而不是先將它們封裝在接口/類中。 例如,在這裏您可以使用一個帶有GetLocalTime()方法的IClock接口,您的類可以使用該方法,而不是直接使用系統函數。這允許您在運行時注入一個SystemClock類,在測試期間注入一個MockClock。通過這樣做,您可以完全控制將什麼時間返回給您的系統/正在測試的類。這個原則顯然適用於所有其他可能返回不可預知結果的靜態引用。我知道它還增加了另一個需要傳遞給你的類的東西,但它至少可以使預先存在的依賴項顯式化並防止目標帖子在測試期間不斷移動(而不必訴諸於MS Fakes之類的黑魔法)。
  8. 這是一個小點,但在這裏,你的私有財產應該真正場