web-dev-qa-db-fra.com

Moq IServiceProvider/IServiceScope

J'essaie de créer un Mock (à l'aide de Moq) pour un IServiceProvider afin de pouvoir tester ma classe de référentiel:

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

Ma tentative de création de l'objet Mock est la suivante:

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

Je reçois l'erreur suivante:

System.NotSupportedException: Expression fait référence à une méthode qui N'appartient pas à l'objet simulé: x => X.GetRequiredService ()

9
blgrnboy

Comme déjà indiqué, Moq n'autorise pas la configuration de méthodes d'extension.

Dans ce cas cependant, les codes sources desdites méthodes d’extension sont disponibles sur Github.

ServiceProviderServiceExtensions .

La solution habituelle à un problème comme celui-ci est de savoir ce que font les méthodes d’extension et de simuler un chemin en toute sécurité lors de son exécution.

Le type de base utilisé est IServiceProvider et sa méthode object Getservice(Type type). Cette méthode est ce qui est appelé en fin de compte lors de la résolution du type de service. Et nous ne traitons que de l'abstraction (interfaces), ce qui facilite l'utilisation de 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
//...

Examinez le code ci-dessus et vous verrez comment l’arrangement satisfait le comportement attendu des méthodes d’extension et, par extension, de la méthode testée.

16
Nkosi

Je voudrais dire que lorsque vous avez besoin d'ajouter autant de cérémonie pour simuler une méthode simple, votre code n'est peut-être pas très testable. Donc, une autre option serait de cacher le localisateur de services derrière une interface plus conviviale (et, à mon avis, plus sympa):

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

J'ai seulement implémenté la méthode GetService<T> ici, mais vous pouvez facilement ajouter/supprimer afin que le localisateur réponde mieux à vos besoins. Et un exemple d'utilisation.

public class ALongRunningTask : IRunForALongTime
{
    private readonly IServiceLocator _serviceLocator;

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

    public void Run()
    {
        using (_serviceLocator)
        {
            var repository = _serviceLocator.Get<IRepository>();
        }
    }
}
1
Kjetil Klaussen

Je cherchais aussi cela, mais je n'avais besoin que de me moquer de GetService. J'utilise toujours AutoFac pour générer automatiquement des simulacres. Dans cet exemple, "GetService" renvoie toujours une instance simulée. Vous pouvez modifier le comportement fictif par la suite avec la méthode freeze.

Exemple:

Classe à tester:

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

Test de l'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
            ...
        }

Fournisseur AutoFac personnalisé

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