起订量 IServiceProvider / IServiceScope

Posted

技术标签:

【中文标题】起订量 IServiceProvider / IServiceScope【英文标题】:Moq IServiceProvider / IServiceScope 【发布时间】:2017-11-04 07:28:22 【问题描述】:

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

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;
    

我创建 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);

我收到以下错误:

System.NotSupportedException : 表达式引用了一个方法 不属于模拟对象:x => x.GetRequiredService()

【问题讨论】:

是扩展方法吗?如果是这样,你不能起订量。 是的,我刚看了一下,好像确实是扩展方法。我正在努力了解如何测试这个类,因为它依赖于 IServiceProvider。 检查此答案以获取可能的解决方案。 Mocking Extension Methods with Moq 你为什么要这样做?对IConfigurationDbContext 使用构造函数注入,就像示例 IdentityServer4.EntityFramework 库所做的那样:/ 这甚至是如何编译的? _dbSettings = dbSettings dbSettings 甚至没有定义 【参考方案1】:

如前所述,起订量不允许设置扩展方法。

在这种情况下,上述扩展方法的源代码可在 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
//...

查看上面的代码,您会看到该安排如何满足扩展方法的预期行为以及扩展(没有双关语)被测方法。

【讨论】:

这里模拟范围的代码应该是它自己的 Nuget 包。感谢您提供这个 - 我的大脑正在融化,试图弄清楚它想要什么。 什么是“ConfigurationDbContext”? @IPS 实体框架中数据库上下文类的名称。根据您生成它的方式,它可以有不同的名称。【参考方案2】:

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

【讨论】:

你能解释一下为什么我们不应该模拟不是我们自己创建的类型吗?我觉得这是一件完全可以接受的事情。这是否与第三方更改其类型功能的可能性有关?如果是这样,那在你的测试中不是一件好事吗?我尽量不要为了抽象而创建抽象,你知道吗? 模拟您无法更改的内容不会为您的测试增加任何价值,这将证明花费在设置模拟上的时间是合理的。有一些合法的用例,例如测试边缘情况行为(例如,当 GetService 抛出异常时),或模拟在您的测试中设置昂贵/复杂的外部依赖项(例如,用于电子邮件通知服务的 SMTP 服务器),但是通常,您应该只将这些外部性视为有效且经过测试。在单元测试中模拟的目的是隔离一些代码并将其嵌入到它是单点故障的上下文中。 我完全同意这一点。使用默认实现比尝试模拟所有内容要简单得多using Microsoft.Extensions.DependencyInjection; var serviceCollection = new ServiceCollection(); serviceCollection.AddScoped(sp =&gt; yourMock.Object); serviceProvider = serviceCollection.BuildServiceProvider();【参考方案3】:

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

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;
    

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

public class ALongRunningTask : IRunForALongTime

    private readonly IServiceLocator _serviceLocator;

    public ALongRunningTask(IServiceLocator serviceLocator)
    
        _serviceLocator = serviceLocator;
    

    public void Run()
    
        using (_serviceLocator)
        
            var repository = _serviceLocator.Get<IRepository>();
        
    

【讨论】:

【参考方案4】:

以防万一它对某人有用,这里有一个示例,说明我如何按照here 的建议为我的单元测试创​​建自己的 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);
    

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

这是正在测试的代码:

using (var scope = _serviceScopeFactory.CreateScope())

    var logger = scope.ServiceProvider.GetRequiredService<ILogger>();
    ...

【讨论】:

【参考方案5】:

我也在寻找这个,但我只需要模拟 GetService。我总是使用 AutoFac 来自动生成模拟。在此示例中,“GetService”始终返回一个模拟实例。之后您可以使用 freeze 方法更改模拟行为。

示例:

要测试的类:

public class ApiResourceRepository : ApiResourceRepository 
            private readonly IServiceProvider _serviceProvider;

            public ApiResourceRepository(IServiceProvider serviceProvider) 
                _serviceProvider = serviceProvider;
            

            public object Get(int id) 
                using (var serviceScope = _serviceProvider.CreateScope()) 
                    var repo = serviceScope.ServiceProvider.GetService<IPersonRepository>();
                    return repo.GetById(id);
                
            
        

单元测试:

 [Fact]
        public void Test() 
            // arrange
            var fixture = new Fixture()
             .Customize(new AutoMoqCustomization())
             .Customize(new ServiceProviderCustomization());

            fixture.Freeze<Mock<IPersonRepository>>()
                .Setup(m => m.GetById(It.IsAny<int>()))
                .Returns(new Person(Name = "John"));

            // Act
            var apiResource = _fixture.Create<ApiResourceRepository>();
            var person = apiResource.Get(1);

            // Assert
            ...
        

自定义 AutoFac 提供程序

public class ServiceProviderCustomization : ICustomization 

        public void Customize(IFixture fixture) 
            var serviceProviderMock = fixture.Freeze<Mock<IServiceProvider>>();

            // GetService
            serviceProviderMock
               .Setup(m => m.GetService(It.IsAny<Type>()))
               .Returns((Type type) => 
                   var mockType = typeof(Mock<>).MakeGenericType(type);
                   var mock = fixture.Create(mockType, new SpecimenContext(fixture)) as Mock;

                   // Inject mock again, so the behavior can be changed with _fixture.Freeze()
                   MethodInfo method = typeof(FixtureRegistrar).GetMethod("Inject");
                   MethodInfo genericMethod = method.MakeGenericMethod(mockType);
                   genericMethod.Invoke(null, new object[]  fixture, mock );

                   return mock.Object;
               );

            // Scoped
            var serviceScopeMock = fixture.Freeze<Mock<IServiceScope>>();
            serviceProviderMock
               .As<IServiceScopeFactory>()
               .Setup(m => m.CreateScope())
               .Returns(serviceScopeMock.Object);

            serviceProviderMock.As<ISupportRequiredService>()
                .Setup(m => m.GetRequiredService(typeof(IServiceScopeFactory)))
                .Returns(serviceProviderMock.Object);
        
    

【讨论】:

【参考方案6】:

免责声明:嵌入的链接指向我的 GitHub 和 NuGet 页面的子页面。但我希望它对您或其他人有所帮助,绝不会少。


我刚刚创建了这样的东西,因为我找不到任何东西。它实现了 IServiceCollection 和 IServiceProvider 来测试我的 Startup-Configuration,特别是是否所有类型都正确注册到 DI-Container。它是这些接口的通用替代品,为每个注册类型提供 Mocks (Moq) 作为单例。 Foo 与 Foo 不同。

GitHub上有一个readme.md,代码库不大。

还有一个 nuget package called MockProvider 和 - 如前所述 - code is on GitHub。我把它放在麻省理工学院,所以用它做你想做的事。它可以免费使用和贡献。

认为这是一种回馈的方式。

【讨论】:

【参考方案7】:

这是我的 sn-p,用于在作用域服务提供者内部进行模拟。 用于测试 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;

用法:

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

【讨论】:

【参考方案8】:

我使用 Moq 和 xUnit 进行测试。我之前遇到过类似的问题,我的解决方案是将数据事务提取到具有接口的 SqlExecuter 类中,这样我就可以直接模拟来自数据库的响应。这简化了一切,足以构建服务提供程序并将其传入。您将需要 xUnit、Moq 和一些 Microsoft 包(Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.InMemory)。

SqlExecuter.cs

public interface ISqlExecuter

    Task<List<SqlParameter>> FirstOrDefaultApiResource(ConfigurationDbContext context, int id);


public class SqlExecuter : ISqlExecuter

    public async Task<ApiResource> FirstOrDefaultApiResource(ConfigurationDbContext context, int id) =>
        return await context.ApiResources
            .Include(x => x.Scopes)
            .Include(x => x.UserClaims)
            .FirstOrDefaultAsync(x => x.Id == id);
 

ApiResourceRepository.cs

public class ApiResourceRepository : IApiResourceRepository

    private readonly IServiceProvider _serviceProvider;
    private readonly ISqlExecuter _sqlExecuter;

    public ApiResourceRepository(IServiceProvider serviceProvider, ISqlExecuter sqlExecuter)
    
        _serviceProvider = serviceProvider;
        _sqlExecuter = sqlExecuter;
        _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 _sqlExecuter.FirstOrDefaultApiResource(context, id);
        

        return result;
    

ApiResourceRepositoryTests.cs

[Fact]
public async Task Get_Success()

    // Arrange
    var id = 42069;
    var scope = "Scope";
    var claim = "UserClaims";
    var services = new ServiceCollection();
    services.AddDbContext<ConfigurationDbContext>(opt => opt
            .UseInMemoryDatabase(databaseName: $"ConfigurationDbContext- DateTime.Now.ToString() ")
            .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)),
        ServiceLifetime.Singleton, ServiceLifetime.Singleton);
    var serviceProvider = services.BuildServiceProvider();
    var mockSqlExecuter = new Mock<SqlExecuter>();
    mockSqlExecuter.Setup(x => x.FirstOrDefaultApiResource(It.IsAny<ConfigurationDbContext>(), It.IsAny<int>()))
        .Returns(new ApiResource()  Id = id , Scope = scope, UserClaims = claim )
    var mockApiResourceRepository = new Mock<ApiResourceRepository>(serviceProvider, mockSqlExecuter.Object);

    // Act
    var result = await mockApiResourceRepository.Object.Get(id);

    
    // Assert
    Assert.NotNull(response);
    Assert.Equal(id, result.Id);
    Assert.Equal(scope, result.Scope);
    Assert.Equal(claim, result.UserClaims);

在其他情况下,我已经植入了在服务提供者中设置的上下文,而不是使用 SqlExecuter 类。

// Arrange
var id = 42069;
var scope = "Scope";
var claim = "UserClaims";
var services = new ServiceCollection();
services.AddDbContext<ConfigurationDbContext>(opt => opt
        .UseInMemoryDatabase(databaseName: $"ConfigurationDbContext-DateTime.Now.ToString()")
        .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)),
    ServiceLifetime.Singleton, ServiceLifetime.Singleton);
var serviceProvider = services.BuildServiceProvider();
var context = Interfaces.ServiceProvider.GetService<ComQueMDSContext>();
context.ApiResources.RemoveRange(context.ApiResources);
context.ApiResources.AddRange(new List<ApiResource>() new ApiResource() Id = id, Scope = scope, UserClaims = claim  );
context.SaveChanges();
var mockApiResourceRepository = new Mock<ApiResourceRepository>(serviceProvider);

按照 xUnit 的建议,我还将大部分工作提取到了 Fixture 类和集合中,以集中上下文并减少测试时间。

https://xunit.net/docs/shared-context

【讨论】:

以上是关于起订量 IServiceProvider / IServiceScope的主要内容,如果未能解决你的问题,请参考以下文章

带参数的起订量 ReturnsAsync()

如何为起订量中的属性赋值?

使用最小起订量模拟静态属性

未找到统一起订量

如何最小起订量委托回调和 lambda 方法

起订量,严格与宽松的使用