测试友好的架构

时间:2015-10-14 17:26:47

标签: c# unit-testing testing dependency-injection service-locator

我有一个关于设计类的最佳方法的问题,以便测试友好。假设我有一个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类提供了一堆属性。当我扩展我的程序并在各种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());
}

我喜欢在调用构造函数时服务定位器模式如何导致更少的代码,但它也提供了更少的灵活性。

建议使用依赖于其他几个服务和存储库的Service类的建议方法是什么,以便可以轻松测试它们?上面显示的方式中的一种或两种都是好的,还是有更好的方式?

2 个答案:

答案 0 :(得分:7)

只是一个非常快速的答案,让你走上正确的轨道。 根据我的经验,如果你的目标是易于测试的代码,你往往会得到干净的可维护代码作为一个很好的副作用。 :-)

要记住的一些要点:

  1. SOLID原则将真正帮助您创建良好,干净,可测试的代码。

    (S + O + I)将此服务分解为只执行一项操作的较小服务,因此只有一个原因需要更改。至少下订单和检查订单的状态是完全不同的事情。如果您对此深入思考,那么您并不需要遵循最明显的步骤(例如,检查信用卡 - >检查库存 - >检查运费),其中一些可以不按顺序完成 - 但是那可能需要不同商业模式的另一个故事。无论如何,如果你真的需要,你可以使用Facade模式在这些较小的服务之上创建一个简化的视图。

  2. 使用IoC容器(例如统一)

  3. 使用Mocking框架(例如Moq)

  4. 服务定位器模式实际上被认为是反模式/代码气味 - 所以请不要使用它。

  5. 您的测试应使用与实际代码相同的路径,因此请删除' Normal构造函数'。用于测试的'构造函数'在你的第一个例子中,你的构造函数应该是什么样的。
  6. 不要在您的类中实例化所需的服务 - 它们应该作为接口传入。 IoC容器将帮助您处理此部分。通过这样做,您将遵循D in Solid(依赖性反转原则)
  7. 尽量避免在您自己的类中直接使用/引用静态类/方法。在这里,我直接讨论使用DateTime.Now()这样的东西,而不是先将它们包装在接口/类中。 例如,在这里你可以有一个带有GetLocalTime()方法的IClock接口,你的类可以使用它而不是直接使用系统函数。这允许您在运行时注入SystemClock类,在测试期间注入MockClock。通过这样做,您可以完全控制确切的时间返回到您测试的系统/类。这个原则显然适用于所有其他可能返回不可预测结果的静态引用。我知道它增加了你需要传递到你的课程中的另一件事,但它至少使预先存在的依赖关系明确,并防止目标帖子在测试期间不断移动(不必诉诸黑魔法,如MS Fakes)。
  8. 这是一个小问题,但这里的私有属性应该是字段

答案 1 :(得分:1)

代码是"可测试的"和松散耦合的代码。

使用DI的主要目的是松耦合。可测试性是从松散耦合的代码中获得的附带好处。但是可测试的代码并不一定是松散耦合的。

虽然注入服务定位器显然比静态引用更加松散,但它仍然不是最佳实践。最大的缺点是lack of transparency of dependencies。您现在可以通过实现服务定位器来保存几行代码然后认为您正在获胜,但是当您实际必须撰写您的应用程序时,这样做会带来任何损失。查看intellisense中的构造函数以确定类具有哪些依赖关系以查找该类的源代码以尝试计算它具有的依赖关系,这是一个明显的优势。

所以,正如您可能已经猜到的那样,我建议您使用构造函数注入。但是,您的示例中还有一个称为bastard injection的反模式。混蛋注入的主要缺点是你通过在内部对它们进行新的紧密耦合。这可能看似无辜,但如果您需要将服务移动到单独的库中会发生什么?很有可能会在您的应用程序中导致循环依赖。

处理此问题的最佳方法(特别是在处理服务而不是配置设置时)是使用pure DI或DI容器,只需要一个构造函数。您还应该使用guard子句来确保无法在没有任何依赖关系的情况下创建订单服务。

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容器可以通过使用基于约定的配置来支付股息。