Moq IServiceProvider/IServiceScope

我正在尝试为 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);

我收到以下错误:

表达式引用的方法 不属于模拟对象: x = > ()

48597 次浏览

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

但是在这种情况下,所述扩展方法的源代码可以在 Github 上获得

ServiceProviderServiceExtended .

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

所有这些的基本类型是 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
//...

查看上面的代码,您将看到这种安排如何满足扩展方法的预期行为,并通过扩展(无双关意思)来满足测试中的方法。

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

例如:

测试类:

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
...
}

自动工厂供应商

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

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

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

免责声明: 嵌入式链接指向我的 GitHub 和 NuGet 页面的子页面。但我希望这能帮到你或者其他人。


我创造了这样的东西,因为我找不到。它实现 IServiceCollection 和 IServiceProvider 来测试我的启动配置,特别是,是否所有类型都正确地注册到 DI-Container。它是这些接口的通用替代品,为每个注册类型提供了 Mocks (Moq)作为单例。Foo < Bar > 与 Foo < Bus > 不同。

GitHub 上有一个 readme.md,代码库也没有那么大。

还有一个 名为 MockProvider 的 nuget 包和-如上所述-的 代码在 GitHub 上。我把它放在麻省理工学院名下了,你想怎么处理都行。它是免费的使用和贡献。

就当是回报吧。

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

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

SqlExecter.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);
}

或者,我不使用 SqlExecter 类,而是在其他情况下对服务提供程序中设置的上下文进行种子化。

// 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

为了防止它对某些人有用,这里有一个例子,说明我如何按照建议的 给你为单元测试创建我自己的 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>();
...
}

下面是我的代码片段,用于嘲笑范围内的服务提供商。 对测试 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)