302 votes

Test unitaire .Net Core - Mock IOptions<T>

J'ai l'impression de passer à côté de quelque chose de vraiment évident. J'ai des classes qui nécessitent l'injection d'options à l'aide du modèle IOptions de .Net Core ( ?). Lorsque je vais tester cette classe, je veux simuler différentes versions des options pour valider la fonctionnalité de la classe. Quelqu'un sait-il comment simuler/instaurer/populer correctement les IOptions en dehors de la classe Startup ?

Voici quelques exemples de classes avec lesquelles je travaille :

Paramètres/Options Modèle

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace OptionsSample.Models
{
    public class SampleOptions
    {
        public string FirstSetting { get; set; }
        public int SecondSetting { get; set; }
    }
}

Classe à tester qui utilise les paramètres :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OptionsSample.Models
using System.Net.Http;
using Microsoft.Extensions.Options;
using System.IO;
using Microsoft.AspNetCore.Http;
using System.Xml.Linq;
using Newtonsoft.Json;
using System.Dynamic;
using Microsoft.Extensions.Logging;

namespace OptionsSample.Repositories
{
    public class SampleRepo : ISampleRepo
    {
        private SampleOptions _options;
        private ILogger<AzureStorageQueuePassthru> _logger;

        public SampleRepo(IOptions<SampleOptions> options)
        {
            _options = options.Value;
        }

        public async Task Get()
        {
        }
    }
}

Test unitaire dans un assemblage différent des autres classes :

using OptionsSample.Repositories;
using OptionsSample.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

namespace OptionsSample.Repositories.Tests
{
    public class SampleRepoTests
    {
        private IOptions<SampleOptions> _options;
        private SampleRepo _sampleRepo;

        public SampleRepoTests()
        {
            //Not sure how to populate IOptions<SampleOptions> here
            _options = options;

            _sampleRepo = new SampleRepo(_options);
        }
    }
}

0 votes

Confondez-vous le sens du mot "moquerie" ? Vous vous moquez d'une interface et la configurez pour qu'elle renvoie une valeur spécifiée. Pour IOptions<T> il suffit de se moquer Value pour retourner la classe que vous désirez

563voto

Necoras Points 2055

Vous devez créer et remplir manuellement un IOptions<SampleOptions> objet. Vous pouvez le faire via la fonction Microsoft.Extensions.Options.Options classe d'aide. Par exemple :

IOptions<SampleOptions> someOptions = Options.Create<SampleOptions>(new SampleOptions());

Vous pouvez simplifier un peu ça :

var someOptions = Options.Create(new SampleOptions());

Il est évident que cela n'est pas très utile tel quel. Vous devrez créer et remplir un objet SampleOptions et le passer à la méthode Create.

2 votes

J'apprécie toutes les réponses supplémentaires qui montrent comment utiliser Moq, etc., mais cette réponse est si simple que c'est définitivement celle que j'utilise. Et ça marche très bien !

1 votes

Excellente réponse. C'est beaucoup plus simple que de s'appuyer sur un cadre de simulation.

2 votes

Merci. J'étais tellement fatigué d'écrire new OptionsWrapper<SampleOptions>(new SampleOptions()); partout

101voto

patvin80 Points 341

Si vous avez l'intention d'utiliser le Mocking Framework comme indiqué par @TSeng dans le commentaire, vous devez ajouter la dépendance suivante dans votre fichier project.json.

   "Moq": "4.6.38-alpha",

Une fois la dépendance rétablie, l'utilisation du cadre MOQ est aussi simple que la création d'une instance de la classe SampleOptions et ensuite, comme mentionné, son affectation à la valeur.

Voici un aperçu du code auquel il ressemblerait.

SampleOptions app = new SampleOptions(){Title="New Website Title Mocked"}; // Sample property
// Make sure you include using Moq;
var mock = new Mock<IOptions<SampleOptions>>();
// We need to set the Value of IOptions to be the SampleOptions Class
mock.Setup(ap => ap.Value).Returns(app);

Une fois que l'objet fantaisie est configuré, vous pouvez maintenant passer l'objet fantaisie au contructeur en tant que

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

Pour votre information, j'ai un dépôt git qui décrit ces deux approches. Github/patvin80

1 votes

Cela devrait être la réponse acceptée, elle fonctionne parfaitement.

0 votes

J'aimerais vraiment que cela fonctionne pour moi, mais ce n'est pas le cas :( Moq 4.13.1

0 votes

@alessandrocb ainsi que la réponse acceptée.

41voto

aleha Points 992

Vous pouvez éviter d'utiliser le MOQ. Utilisez dans vos tests le fichier de configuration .json. Un seul fichier pour plusieurs fichiers de classe de test. Il est possible d'utiliser ConfigurationBuilder dans ce cas.

Exemple de appsetting.json

{
    "someService" {
        "someProp": "someValue
    }
}

Exemple de classe de cartographie des paramètres :

public class SomeServiceConfiguration
{
     public string SomeProp { get; set; }
}

Exemple de service à tester :

public class SomeService
{
    public SomeService(IOptions<SomeServiceConfiguration> config)
    {
        _config = config ?? throw new ArgumentNullException(nameof(_config));
    }
}

Classe de test NUnit :

[TestFixture]
public class SomeServiceTests
{

    private IOptions<SomeServiceConfiguration> _config;
    private SomeService _service;

    [OneTimeSetUp]
    public void GlobalPrepare()
    {
         var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", false)
            .Build();

        _config = Options.Create(configuration.GetSection("someService").Get<SomeServiceConfiguration>());
    }

    [SetUp]
    public void PerTestPrepare()
    {
        _service = new SomeService(_config);
    }
}

0 votes

Cela a bien fonctionné pour moi, merci ! Je ne voulais pas utiliser Moq pour quelque chose qui semblait si simple et je ne voulais pas essayer de remplir mes propres options avec des paramètres de configuration.

6 votes

Cela fonctionne très bien, mais l'information essentielle manquante est que vous devez inclure le paquet nuget Microsoft.Extensions.Configuration.Binder, sinon vous n'aurez pas la méthode d'extension "Get<SomeServiceConfiguration>" disponible.

0 votes

J'ai dû exécuter dotnet add package Microsoft.Extensions.Configuration.Json pour que cela fonctionne. Excellente réponse !

16voto

Frank Points 1419

Classe donnée Person qui dépend de PersonSettings comme suit :

public class PersonSettings
{
    public string Name;
}

public class Person
{
    PersonSettings _settings;

    public Person(IOptions<PersonSettings> settings)
    {
        _settings = settings.Value;
    }

    public string Name => _settings.Name;
}

IOptions<PersonSettings> peuvent faire l'objet de moqueries et Person peut être testé comme suit :

[TestFixture]
public class Test
{
    ServiceProvider _provider;

    [OneTimeSetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        // mock PersonSettings
        services.AddTransient<IOptions<PersonSettings>>(
            provider => Options.Create<PersonSettings>(new PersonSettings
            {
                Name = "Matt"
            }));
        _provider = services.BuildServiceProvider();
    }

    [Test]
    public void TestName()
    {
        IOptions<PersonSettings> options = _provider.GetService<IOptions<PersonSettings>>();
        Assert.IsNotNull(options, "options could not be created");

        Person person = new Person(options);
        Assert.IsTrue(person.Name == "Matt", "person is not Matt");    
    }
}

Pour injecter IOptions<PersonSettings> en Person au lieu de le passer explicitement au ctor, utilisez ce code :

[TestFixture]
public class Test
{
    ServiceProvider _provider;

    [OneTimeSetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddTransient<IOptions<PersonSettings>>(
            provider => Options.Create<PersonSettings>(new PersonSettings
            {
                Name = "Matt"
            }));
        services.AddTransient<Person>();
        _provider = services.BuildServiceProvider();
    }

    [Test]
    public void TestName()
    {
        Person person = _provider.GetService<Person>();
        Assert.IsNotNull(person, "person could not be created");

        Assert.IsTrue(person.Name == "Matt", "person is not Matt");
    }
}

0 votes

Vous ne testez rien d'utile. Le framework DI de Microsoft est déjà testé en unité. En l'état actuel des choses, il s'agit vraiment d'un test d'intégration (intégration avec un framework tiers).

7 votes

@ErikPhilips Mon code montre comment simuler IOptions<T> comme demandé par l'OP. Je suis d'accord pour dire que cela ne teste rien d'utile en soi, mais cela peut être utile pour tester autre chose.

11voto

Robert Corvus Points 845

Voici une autre méthode simple qui ne nécessite pas Mock, mais qui utilise plutôt OptionsWrapper :

var myAppSettingsOptions = new MyAppSettingsOptions();
appSettingsOptions.MyObjects = new MyObject[]{new MyObject(){MyProp1 = "one", MyProp2 = "two", }};
var optionsWrapper = new OptionsWrapper<MyAppSettingsOptions>(myAppSettingsOptions );
var myClassToTest = new MyClassToTest(optionsWrapper);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X