【问题标题】:How to mock IServiceProvider and still allow CreateInstance of generic types?如何模拟 IServiceProvider 并仍然允许泛型类型的 CreateInstance?
【发布时间】:2025-12-15 05:00:02
【问题描述】:

我正在尝试对一段代码进行单元测试,该代码使用IServiceProvider 和反射的组合来创建扩展抽象类BaseCommand 的每个类的实例:

IEnumerable<BaseCommand> commandsInAssembly = typeof(BaseCommand)
    .Assembly.GetTypes()
    .Where(t => t.IsSubclassOf(typeof(BaseCommand)) && !t.IsAbstract)
    .Select(t => (BaseCommand)ActivatorUtilities.CreateInstance(_serviceProvider, t))
    .ToList();

这里的棘手部分是_serviceProvider 被注入并且需要被模拟(我认为),以允许这段代码成功且独立地运行。每个命令都需要访问 DI 以解决其依赖关系。大多数命令看起来类似于:

public SomeCommand(IAppState appState, ILoggerAdapter<SomeCommand> logger) : base(appState)

我能够很好地模拟 IServiceProvider 以解决 IAppState,但我在使用 ILoggerAdapter&lt;&gt; 时遇到了困难。这是我目前的设置:

单元测试构造函数

var serviceProvider = new Mock<IServiceProvider>();

serviceProvider
    .Setup(x => x.GetService(typeof(IAppState)))
    .Returns(new AppState());

serviceProvider
    .Setup(x => x.GetService(typeof(ILoggerAdapter<>)))
    .Returns(typeof(LoggerAdapter<>));

var serviceScope = new Mock<IServiceScope>();
serviceScope
    .Setup(x => x.ServiceProvider)
    .Returns(serviceProvider.Object);

var serviceScopeFactory = new Mock<IServiceScopeFactory>();
serviceScopeFactory
    .Setup(x => x.CreateScope())
    .Returns(serviceScope.Object);

serviceProvider
    .Setup(x => x.GetService(typeof(IServiceScopeFactory)))
    .Returns(serviceScopeFactory.Object); var mocker = new AutoMocker();

_commandDispatcher = new CommandDispatcher(serviceProvider.Object, _mockAppState.Object, _mockLogger.Object);

这会产生以下错误: System.InvalidOperationException:尝试激活“SomeCommand”时无法解析“ILoggerAdapter`1[SomeCommand]”类型的服务。

如果我尝试更明确地设置我的设置(我想避免这样做,这会使测试变得更加脆弱)并使用:

serviceProvider
    .Setup(x => x.GetService(typeof(ILoggerAdapter<SomeCommand>)))
    .Returns(typeof(LoggerAdapter<SomeCommand>));

但这也会产生错误:System.ArgumentException : 'System.RuntimeType' 类型的对象无法转换为 'ILoggerAdapter`1[SomeCommand]' 类型。

我了解到使用 AutoMocking 容器或 Fixture 可能更合适,但我不确定从哪里开始。我对 C# 中的单元测试相当陌生。

如何模拟/向我的 SUT 提供 IServiceProvider 而不会导致 ActivatorUtilities.CreateInstance(IServiceProvider, type) 爆炸?

【问题讨论】:

  • 我认为,如果您觉得这太难测试,我建议您没有以最佳方式构建它
  • 这是可能的。我希望这更多是我缺乏测试经验的问题。这感觉像是一个简单的起订量配置问题,我只是不太了解。
  • 所以我不会太担心创建了正确的实例,更多的是你应该做mock.Setup(foo =&gt; foo.CreateInstance(It.Is&lt;SomeType&gt;())).Returns(new MyMockType); 然后做两次检查,首先是mock.Verify() 以确保调用该方法,其次是检查实例的类型是 MyMockType

标签: c# unit-testing asp.net-core moq xunit


【解决方案1】:
serviceProvider
    .Setup(x => x.GetService(typeof(ILoggerAdapter<>)))
    .Returns(typeof(LoggerAdapter<>));

此设置的问题是 typeof(ILoggerAdapter&lt;&gt;) 永远不会被解析,它是一个泛型类型,所以 ILoggerAdapter&lt;SomeCommand&gt; 将被解析。

serviceProvider
    .Setup(x => x.GetService(typeof(ILoggerAdapter<SomeCommand>)))
    .Returns(typeof(LoggerAdapter<SomeCommand>));

通过此设置,您将解析正确的服务。但是,当您返回代表LoggerAdapter&lt;SomeCommand&gt;Type 实例而不是LoggerAdapter&lt;SomeCommand&gt;实例时,您返回错误的结果。您将需要通过new 来创建LoggerAdapter&lt;SomeCommand&gt; 的实例,或者模拟它。


另一个解决方案可能是您不模拟 IServiceProvider 实例,而是使用常规 DI 设置创建一个“真实的”IServiceProvider 实例:创建一个新的 ServiceCollection 实例,添加您的服务并调用 @987654333 @。例如:

var services = new ServiceCollection();
// Add IAppState, ILoggerAdapater, and other services

// Create the service provider instance
var serviceProvider = services.BuildServiceProvider();

// Resolve services from the IServiceProvider and pass it along
var appState = serviceProvider.GetRequiredService<IAppState>();

【讨论】:

    【解决方案2】:

    您的 SUT 不调用 IServiceProvider 的方法,因此根本不需要模拟它们。您想要测试的只是对于每个 BaseCommand 的具体子类,SUT 是否通过 _serviceProvidertCreateInstance

    一种方法是将静态方法ActivatorUtilities.CreateInstance 转换为CommandDispatcher 的可注入依赖项,例如

    interface IActivator
    {
        object CreateInstance(IServiceProvider serviceProvider, Type t)
    }
    

    那么测试可能是这样的

    pivate class TestCommand : BaseCommand
    {
        public TestCommand(Type realCommandType)
        {
        }
    }
    
    // ...
    
    // that's all the IServiceProvider mocking you need
    var serviceProvider = new Mock<IServiceProvider>();
    
    var activator = new Mock<IActivator>();
    activator.Setup(_ => _.CreateInstance(serviceProvider, It.IsAny<Type>())
        .Returns<IServiceProvider, Type>((sp, t) => new TestCommand(t));
    
    // ...
    
    foreach (var expectedType in typeof(CommandDispatcher).Assembly.GetTypes()
       .Where(t => t.IsSubclassOf(typeof(BaseCommand)) && !t.IsAbstract))
    {
        // check, whether whatever you do with commandsInAssembly 
        // contains a TestCommand with expectedType 
    }
    

    【讨论】:

    • 我认为这大大简化了模拟代码,并且很有意义。唯一让我感到奇怪的部分是注入静态方法以使这种方法成为可能。我不确定我是否会进行更改以使测试更容易,或者这是否也是更好的设计决策,并且测试问题说明了这一点。