Moq IServiceProvider/IServiceScope
Я пытаюсь создать Mock (используя 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;
}
}
Моя попытка создания объекта 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()
Ответы
Ответ 1
Как уже говорилось, 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
//...
Просмотрите приведенный выше код, и вы увидите, как компоновка удовлетворяет ожидаемому поведению методов расширения и, соответственно, расширения (без каламбура) тестируемого метода.
Ответ 2
Я тоже искал это, но мне только нужно было издеваться над GetService. Я всегда использую AutoFac для автоматического создания mocks. В этом примере "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);
}
}
}
Unit тест:
[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);
}
}
Ответ 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<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>();
}
}
}