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