Moq IServiceProvider/IServiceScope

blg*_*boy 18 c# unit-testing moq .net-core asp.net-core

我正在尝试创建一个模拟(使用Moq),IServiceProvider以便我可以测试我的存储库类:

public class ApiResourceRepository : IApiResourceRepository
{
    private readonly IServiceProvider _serviceProvider;

    public ApiResourceRepository(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _dbSettings = dbSettings;
    }

    public async Task<ApiResource> Get(int id)
    {
        ApiResource result;

        using (var serviceScope = _serviceProvider.
            GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
            result = await
                context.ApiResources
                .Include(x => x.Scopes)
                .Include(x => x.UserClaims)
                .FirstOrDefaultAsync(x => x.Id == id);
        }

        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

我创建Mock对象的尝试如下:

Mock<IServiceProvider> serviceProvider = new Mock<IServiceProvider>();

serviceProvider.Setup(x => x.GetRequiredService<ConfigurationDbContext>())
    .Returns(new ConfigurationDbContext(Options, StoreOptions));

Mock<IServiceScope> serviceScope = new Mock<IServiceScope>();

serviceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object);

serviceProvider.Setup(x => x.CreateScope()).Returns(serviceScope.Object);
Run Code Online (Sandbox Code Playgroud)

我收到以下错误:

System.NotSupportedException:Expression引用不属于模拟对象的方法:x => x.GetRequiredService()

Nko*_*osi 35

如前所述,Moq不允许设置扩展方法.

然而,在这种情况下,所述扩展方法的源代码在Github上可用

ServiceProviderServiceExtensions.

解决这类问题的常用方法是找出扩展方法的作用,并通过它的执行安全地模拟路径.

所有这些中的基本类型是IServiceProvider它的object Getservice(Type type)方法.此方法是解析服务类型时最终调用的方法.而且我们只处理抽象(接口),这使得使用moq变得更加容易.

//Arrange
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider
    .Setup(x => x.GetService(typeof(ConfigurationDbContext)))
    .Returns(new ConfigurationDbContext(Options, StoreOptions));

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 sut = new ApiResourceRepository(serviceProvider.Object);

//Act
var actual = sut.Get(myIntValue);

//Asssert
//...
Run Code Online (Sandbox Code Playgroud)

查看上面的代码,您将看到该安排如何满足扩展方法的预期行为,并通过扩展(无双关语)测试方法.

  • 这里用于模拟范围的代码应该是它自己的 Nuget 包。感谢您提供此信息 - 我的大脑正在融化,试图弄清楚它想要什么。 (5认同)

小智 14

以防万一它对某人有用,这里有一个示例,说明我如何按照此处的建议为我的单元测试创​​建自己的 ServiceProvider 。我还添加了 ServiceScope 和 ServiceScopeFactory 模拟来提供所有服务。

这是我的单元测试中的代码:

var serviceCollection = new ServiceCollection();

// Add any DI stuff here:
serviceCollection.AddSingleton<ILogger>(loggerMock.Object);

// Create the ServiceProvider
var serviceProvider = serviceCollection.BuildServiceProvider();

// serviceScopeMock will contain my ServiceProvider
var serviceScopeMock = new Mock<IServiceScope>();
serviceScopeMock.SetupGet<IServiceProvider>(s => s.ServiceProvider)
    .Returns(serviceProvider);

// serviceScopeFactoryMock will contain my serviceScopeMock
var serviceScopeFactoryMock = new Mock<IServiceScopeFactory>();
serviceScopeFactoryMock.Setup(s => s.CreateScope())
    .Returns(serviceScopeMock.Object);
    
Run Code Online (Sandbox Code Playgroud)

然后我可以将 serviceScopeFactoryMock 传递给 sut 构造函数。

这是正在测试的代码:

using (var scope = _serviceScopeFactory.CreateScope())
{
    var logger = scope.ServiceProvider.GetRequiredService<ILogger>();
    ...
}
Run Code Online (Sandbox Code Playgroud)


Már*_*ssa 7

一般规则是您不要模拟您不拥有的类型。除非您需要验证对服务提供者的调用,否则只需在您的测试中IServiceProvider从 a构建ServiceCollection

  • 我完全同意这种说法。使用默认实现比尝试模拟所有内容“using Microsoft.Extensions.DependencyInjection;”要简单得多。var serviceCollection = new ServiceCollection(); serviceCollection.AddScoped(sp =&gt; yourMock.Object); serviceProvider = serviceCollection.BuildServiceProvider();` (10认同)
  • 模拟您无法更改的内容不会为您的测试增加任何价值,从而证明花在设置模拟上的时间是合理的。有一些合法的用例,例如测试边缘情况行为(例如,当 GetService 引发异常时),或者模拟在测试中设置昂贵/复杂的外部依赖项(例如,用于电子邮件通知服务的 SMTP 服务器),但是一般来说,您应该只考虑这些外部性是有效的并且经过测试的。在单元测试中模拟的目的是隔离一些代码并将其嵌入到单点故障的上下文中。 (2认同)

Kje*_*sen 5

我想说的是,当您需要添加那么多仪式只是为了模拟一个简单的方法时,那么您的代码可能不太可测试。因此,另一种选择是将服务定位器隐藏在更加测试和模拟友好的界面后面(在我看来也是一个更好的界面):

public interface IServiceLocator : IDisposable
{
    T Get<T>();
}

public class ScopedServiceLocator : IServiceLocator
{
    private readonly IServiceScopeFactory _factory;
    private IServiceScope _scope;

    public ScopedServiceLocator(IServiceScopeFactory factory)
    {
        _factory = factory;
    }

    public T Get<T>()
    {
        if (_scope == null)
            _scope = _factory.CreateScope();

        return _scope.ServiceProvider.GetService<T>();
    }


    public void Dispose()
    {
        _scope?.Dispose();
        _scope = null;
    }
}
Run Code Online (Sandbox Code Playgroud)

我只GetService<T>在这里实现了该方法,但您可以轻松添加/删除,以便定位器更好地满足您的需求。以及如何使用它的示例;

public class ALongRunningTask : IRunForALongTime
{
    private readonly IServiceLocator _serviceLocator;

    public ALongRunningTask(IServiceLocator serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }

    public void Run()
    {
        using (_serviceLocator)
        {
            var repository = _serviceLocator.Get<IRepository>();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


dim*_*aan 5

这是我在范围服务提供商内部进行模拟的片段。对于测试 IHostedService 等很有用:

Mock<IServiceProvider> CreateScopedServicesProvider(params (Type @interface, Object service)[] services)
{
    var scopedServiceProvider = new Mock<IServiceProvider>();

    foreach (var (@interfcae, service) in services)
    {
        scopedServiceProvider
            .Setup(s => s.GetService(@interfcae))
            .Returns(service);
    }

    var scope = new Mock<IServiceScope>();
    scope
        .SetupGet(s => s.ServiceProvider)
        .Returns(scopedServiceProvider.Object);

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

    var serviceProvider = new Mock<IServiceProvider>();
    serviceProvider
        .Setup(s => s.GetService(typeof(IServiceScopeFactory)))
        .Returns(serviceScopeFactory.Object);

    return serviceProvider;
}
Run Code Online (Sandbox Code Playgroud)

用法:

var service = new Mock<IMyService>();
var serviceProvider = CreateScopedServicesProvider((typeof(IMyService), scopedService.Object));
var sut = new ServiceThatUsesScopes(serviceProvider.Object)
Run Code Online (Sandbox Code Playgroud)