如何模拟IServiceProvider并仍然允许泛型类型的CreateInstance?

时间:2018-08-28 13:30:34

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

我试图对使用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<>时遇到了困难。这是我当前的设置:

单元测试构造器

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#进行单元测试还很陌生。

如何在IServiceProvider不爆炸的情况下为我的SUT模拟/提供ActivatorUtilities.CreateInstance(IServiceProvider, type)

2 个答案:

答案 0 :(得分:3)

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

此设置的问题是typeof(ILoggerAdapter<>)从未得到解决,这是一个通用类型,因此ILoggerAdapter<SomeCommand>将得到解决。

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

通过此设置,您可以解决正确的服务。但是,由于返回的Type实例代表LoggerAdapter<SomeCommand>,而不是LoggerAdapter<SomeCommand>实例,因此您返回的结果是错误的。您将需要通过LoggerAdapter<SomeCommand>对其进行创建或对其进行模拟来创建new的实例。


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

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

答案 1 :(得分:1)

您的SUT不会调用IServiceProvider的方法,因此根本不需要模拟它们。您要测试的是SUT是否将每个BaseCommand的具体子类的_serviceProvidert传递到CreateInstance

一种实现方法是将静态方法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 
}