我试图对使用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)
?
答案 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的具体子类的_serviceProvider
和t
传递到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
}