嘲笑IServiceProvider扩展方法的面临的问题

时间:2018-10-11 18:05:15

标签: c# unit-testing dependency-injection moq

我知道可能会重复这个问题,但是我面临一个无法模拟非静态方法的问题,因为它是调用非静态方法的静态方法。

我的控制器逻辑调用了静态ServiceProviderServiceExtensions方法GetServices<T>(this IServiceProvider provider),而该方法似乎又调用了非静态方法provider.GetService(serviceType)

基本上,我有依赖项注入,其中一个接口有两种实现方式

services.AddSingleton<IProvider, CustomProvider1>();
services.AddSingleton<IProvider, CustomProvider2>();

现在,我有2个控制器,它们直接将对此提供程序的依赖关系视为:

 public Controller1(IProvider provider)
 public Controller2(IProvider provider)

在我的控制器中,我将依存关系解析为

Controller1.cs

provider = serviceProvider.GetServices<IProvider>()
                .FirstOrDefault(lp => lp.GetType() == typeof(CustomeProvider1));
            and

Controller2.cs
provider = serviceProvider.GetServices<IProvider>()
                .FirstOrDefault(lp => lp.GetType() == typeof(CustomeProvider1));

现在,当我尝试如下模拟单元测试时:

 serviceProviderMock
                .Setup(x => x.GetService(typeof(CustomeProvider2)))
                .Returns(a);

我收到错误消息,类型为System.Collections.Generic.IEnumerable [IProvider] has been registered. and I cant directly mock the GetServicesmethod`的服务是静态的。

有什么线索可以模拟我的测试吗? 谢谢。

1 个答案:

答案 0 :(得分:3)

  

我的控制器逻辑调用静态ServiceProviderServiceExtensions方法GetServices(此IServiceProvider提供程序)

这是开始出错的地方。从控制器内部调用GetServices<T>Service Locator anti-pattern的应用程序。您遇到的所有麻烦都源于这种滥用。

相反,您应该:

  1. 仅使用构造函数注入
  2. 切勿向IServiceProvider注入另一种抽象,该抽象表示Composition Root之外的任何类的构造函数中的容器

所以这意味着您应该具有如下构造函数:

public Controller1(IProvider provider)

您的IProvider含糊不清,但让我们假设一下可以。但是,不行的是让应用程序代码处理这种歧义。取而代之的是,您应该只处理“合成根目录”中的这种歧义,可以按照以下步骤进行操作:

services.AddSingleton<CustomProvider1>();
services.AddTransient<Controller1>(c => new Controller1(
    c.GetRequiredService<CustomProvider1>()));

services.AddSingleton<CustomProvider2>();
services.AddTransient<Controller2>(c => new Controller2(
    c.GetRequiredService<CustomProvider2>()));

请注意,默认情况下,ASP.NET Core MVC不会从DI容器解析控制器(这是一个非常非常奇怪的默认设置)。因此,要强制MVC使用DI容器来解析控制器,从而使用上面的注册,您必须添加以下代码:

services.AddMvc()
   .AddControllersAsServices();

上述注册仅在那些控制器仅具有一个依赖性的情况下才能很好地起作用,因为此方法有效地禁用了自动装配。如果该类具有更多的依赖关系,则可以使用以下更易于维护的构造:

services.AddTransient<Controller1>(c =>
    ActivatorUtilities.CreateInstance<Controller1>(
        c,
        c.GetRequiredService<CustomProvider1>()));

services.AddTransient<Controller2>(c =>
    ActivatorUtilities.CreateInstance<Controller2>(
        c,
        c.GetRequiredService<CustomProvider1>()));

这利用了.NET Core的ActivatorUtilities类,该类允许在传递某些依赖项的同时自动装配该类。

请注意,ActivatorUtilities具有某些缺点,例如无法检测循环依赖性。而是会抛出(堆栈)堆栈溢出异常。

但是...正如我之前指出的那样,您的IProvider抽象是模棱两可的,因为有两个实现,而使用者需要一个不同的实现。尽管这本身并不是一件坏事,但您应该始终验证自己是否违反了Liskov Substitution Principle

您可以通过交换两个实现来检查是否违反了LSP。因此,请问问自己:Controller1注入CustomProvider2时会发生什么,Controller2注入CustomProvider1时会发生什么。如果答案是它们将停止工作,则表明您违反了LSP,这是一个设计问题。

如果控制器中断,则意味着这两个实现的行为都非常不同,而消费者应该能够假定所有实现都根据其抽象表现。违反LSP意味着增加了复杂性。

在这种情况下,当您确定自己 违反LSP时,解决方案是为每个实现提供自己的抽象:

interface IProvider1 { }
interface IProvider2 { }

这两个接口是否具有相同的签名无关紧要,因为LSP冲突表明两个接口实际上在行为上有很大不同,因为换出实现会破坏它们的客户端。

但是,请注意,即使直接使用者继续工作,这仍然可能意味着交换实现时,应用程序开始表现不正确。这并不意味着您违反了SRP。例如,当provider1登录到磁盘,provider2登录到数据库时,预期的行为是对controller1的调用将导致磁盘上的登录被追加。因此,实现它不是您想要实现的目标,而是您希望在“合成根目录”中进行配置的东西。这并不表示违反LSP。在这种情况下,合同的行为仍然符合消费者的期望。

如果交换实现对其使用者没有显着影响,则您没有违反LSP,这意味着要进行给定的注册。或者您可以通过为我们提供一个“真实的” DI容器来简化事情;-)