c# - 测试友好的架构

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

我有一个关于设计类以便测试友好的最佳方法的问题。假设我有一个 OrderService 类,用于下新订单、检查订单状态等。该类将需要访问客户信息、库存信息、运输信息等。因此 OrderService 类将需要使用 CustomerService、InventoryService 和 ShippingService。每个服务还有自己的后备存储库。

将 OrderService 类设计为易于测试的最佳方法是什么?我见过的两种常用模式是依赖注入(inject)和服务定位器。对于依赖注入(inject),我会做这样的事情:

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 对象时,都需要大量代码来调用我的测试中的构造函数。我的服务类最终还为它们使用的每个服务和存储库类提供了一堆属性。当我扩展我的程序并在各种服务和存储库类之间添加更多依赖项时,我必须返回并向我已经创建的类的构造函数添加越来越多的参数。

对于服务定位器模式,我可以这样做:

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

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

设置我的服务类的推荐方法是什么,这些类依赖于其他几个服务和存储库,以便可以轻松地对其进行测试?我上面展示的任何一种或两种方法都不错,还是有更好的方法?

最佳答案

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

要记住的一些要点:

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

    (S + O + I) 将这个服务分解成更小的服务,只做一件事,因此只有一个改变的理由。至少下订单和检查订单状态是完全不同的事情。如果你仔细考虑一下,你真的不需要遵循最明显的步骤(例如检查信用->检查库存->检查运输),其中一些可以乱序完成 - 但那是另一回事可能需要不同商业模式的故事。无论如何,如果您确实需要,您可以使用 Facade 模式在这些较小的服务之上创建一个简化的 View 。

  2. 使用 IoC 容器(例如 unity)

  3. 使用模拟框架(例如 Moq)

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

  5. 您的测试应该使用与实际代码相同的路径,因此摆脱“普通构造函数”。第一个示例中的“用于测试的构造函数”是您的构造函数的外观。

  6. 不要在您的类中实例化所需的服务 - 它们应该作为接口(interface)传入。 IoC 容器会帮你处理这部分。这样你就遵循了 Solid 中的 D(依赖倒置原则)

  7. 尽可能避免直接在您自己的类中使用/引用静态类/方法。在这里,我谈论的是直接使用 DateTime.Now() 之类的东西,而不是先将它们包装在接口(interface)/类中。 例如,在这里您可以有一个带有 GetLocalTime() 方法的 IClock 接口(interface),您的类可以使用该方法而不是直接使用系统函数。这允许您在运行时注入(inject)一个 SystemClock 类,在测试期间注入(inject)一个 MockClock。通过这样做,您可以完全控制返回到被测系统/类的确切时间。这一原则显然适用于所有其他可能返回不可预测结果的静态引用。我知道它增加了你需要传递到你的类中的另一件事,但它至少使预先存在的依赖关系明确并防止目标帖子在测试期间不断移动(不必诉诸黑魔法,如 MS Fakes)。

  8. 这是次要的一点,但是你的私有(private)属性应该是字段

关于c# - 测试友好的架构,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33131858/

相关文章:

c# - Entity Framework 4.0 中的反序列化问题

c# - 在 visual studio 中自动生成部分类的扩展

c# - 我的 GUID 是如何损坏的?

c# - 测试函数 "System.Web.Mvc.ViewEngines.Engines.FindPartialView"

java - 如何为您的 Java 应用程序设置 "test mode"?

ruby - 我可以在不更改源代码的情况下使用 redgreen 运行我们的测试吗?

java - 廉价串口设备的推荐

c# - 如何正确使用UrlEncode和Decode

c# - 如何从字符串生成流?

c++ - 暴力破解不同输入导致程序崩溃的策略是什么?