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. 这是一个小点,但在这里,你的私有财产应该真正场