单元测试 Factory/Service 定位器 - 静态 class

Unit Testing Factory/Service Locator - Static class

最近,我看到了这段代码。由于我试图一次学习一些东西,所以我遇到了这段代码的问题,但不知道如何解决。我希望能够对这段代码进行单元测试

public static class CarFactory
{
    private static readonly IDictionary<string, Type> CarsRegistry = new Dictionary<string, Type>();

    public static void Register<TCar>(string car) where TCar : CarBase, new()
    {
        if (!CarsRegistry.ContainsKey(car))
        {
            CarsRegistry.Add(car, typeof(TCar));
        }
    }

    public static ICar Create(string car)
    {
        if (CarsRegistry.ContainsKey(car))
        {
            CarBase newCar = (CarBase)Activator.CreateInstance(CarsRegistry[car]);
            return newCar;
        }

        throw new NotSupportedException($"Unknown '{car}'");
    }
}

我对这段代码没有什么问题。

  1. 名称是 CarFactory,但在我看来这不像是工厂模式。它看起来更像定位器模式
  2. class 是静态的 - 我听说静态 classes 不利于 Moq 等框架中的单元测试,而且它们还隐藏了依赖关系。假设另一个正则 class 中的方法使用此,对于单元测试,无法知道该方法依赖于此静态 class

我想确保这个 class 被正确调用,我认为根据我的阅读,这是定位器模式。

我还想对此 class 进行单元测试,需要帮助才能使用 Moq 对其进行单元测试。

感谢@ErikPhillips 下面的解释,我现在明白使用这个 class 的其他 classes 将不可测试。所以,如果我有一个像下面这样的 class:

public class CarConsumer
{
   public void ICar GetRedCar()
   {
     var result = CarFactory.Create("Tesla");
     result.Color = Color.Red;
     return result;
   }
}

,GetRedCar() 方法将很难测试,因为它使用 CarFactory static class 并且对于单元测试或外部客户端,GetRedCar() 方法 API 中没有任何建议它取决于这个静态 class.

我想重构 CarFactory class 以便其他 class 使用它的人像上面的示例 CarConsumer class 一样可以被正确测试。

I would like to be able to unit test this code

哪些具体问题阻止您对此 class 进行单元测试?它有两种方法,编写单元测试似乎很简单。

Name is CarFactory but this does not look like a Factory Pattern to me

我认为工厂模式

The factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created

我输入了汽车的名称(所以我没有指定类型),它为我创建了 class。差不多就是这样。这是一个很好的例子吗?不是我的看法,但我对它做得有多好的看法不会改变它的现状。

这并不意味着它不是服务定位器,但它绝对是一个工厂方法。 (老实说,它看起来不像服务定位器,因为它只提供单一服务)

unit testing in frameworks like Moq

Moq 不是单元测试框架。 Moq 是一个 模拟框架 。静态 classes 不容易 Mock。如果可以模拟它,则可以使用需要模拟的方法进行单元测试 class.

static classes .. that they also hide dependencies.

任何设计不当的东西都可以做任何事情。根据定义,静态 Class 并非旨在隐藏任何内容。

在这种情况下,我会说,这个 Static Class 会阻止您轻松模拟它以对依赖于 Static Classes 方法的其他方法进行单元测试。

I would also like to unit test this class and need help to make it unit testable using Moq.

同样,没有什么可以阻止您对此进行单元测试 class。

public class CarFactoryTests
{  
  public class MoqCar : CarBase { }

  public void Register_WithValidParameters_DoesNotThrowException
  {
    // Act
    Assert.DoesNotThrow(() => CarFactory.Register<MoqCar>(
      nameof(Register_WithValidParameters_DoesNotThrowException)));
  }

  public void Create_WithValidCar_DoesNotThrowException
  {
    CarFactory.Register<MoqCar>(
      nameof(Create_WithValidParameters_DoesNotThrowException));

    Assert.DoesNotThrow(() => CarFactory.Create(
      nameof(Create_WithValidParameters_DoesNotThrowException));
  }

  // etc etc
}

您可能 运行 遇到的问题是

public class CarConsumer
{
   public void ICar GetRedCar()
   {
     var result = CarFactory.Create("Tesla");
     result.Color = Color.Red;
     return result;
   }
}

测试此方法意味着您无法完全控制该方法,因为有外部代码 GetRedCar() 依赖。你不能在这里写一个纯单元测试。

这就是您必须将 CarFactory 转换为 实例 class 的原因。然后确保它对于您使用的任何 DI 框架都有正确的生命周期。

public class CarConsumer
{
   private ICarFactory _carFactory;
   public CarConsumer(ICarFactory carFactory)
   {
     _carFactory = carFactory;
   }

   public void ICar GetRedCar()
   {
     var result = _carFactory.Create("Tesla");
     result.Color = Color.Red;
     return result;
   }
}

现在我们可以起订 ICarfactory 并针对 GetRedCar().

编写纯单元测试

以下不推荐

如果出于某种原因你被这种类型的工厂所困,但你仍然想编写纯单元测试,你可以这样做:

public class CarConsumer
{
   private Func<string, ICar> _createCar;
   public CarConsumer(Func<string, ICar> createCar= CarFactory.Create)
   {
     _createCar = createCar;
   }

   public void ICar GetRedCar()
   {
     var result = _createCar("Tesla");
     result.Color = Color.Red;
     return result;
   }
}

我们可以 Moq 这种类型的 Func,但它实际上只是解决实际问题的拐杖。

I guess the real question I have is how to make my CarFactory so that methods from other classes using it can be tested using Moq?

public interface ICarFactory
{
  void Register<TCar>(string car) where TCar : CarBase, new();
  ICar Create(string car);
}

public class CarFactory : ICarFactory
{
  private readonly IDictionary<string, Type> CarsRegistry 
    = new Dictionary<string, Type>();

  public void Register<TCar>(string car) where TCar : CarBase, new()
  {
    if (!CarsRegistry.ContainsKey(car))
    {
      CarsRegistry.Add(car, typeof(TCar));
    }
  }

  public ICar Create(string car)
  {
    if (CarsRegistry.ContainsKey(car))
    {
      CarBase newCar = (CarBase)Activator.CreateInstance(CarsRegistry[car]);
      return newCar;
    }

    throw new NotSupportedException($"Unknown '{car}'");
  }
}