Test unitaire .Net Core - IOptions simulées <T>

137

J'ai l'impression de manquer quelque chose de vraiment évident ici. J'ai des classes qui nécessitent l'injection d'options à l'aide du modèle .Net Core IOptions (?). Quand je vais au test unitaire de cette classe, je veux me moquer de différentes versions des options pour valider la fonctionnalité de la classe. Est-ce que quelqu'un sait comment simuler / instancier / remplir correctement les IOptions en dehors de la classe Startup?

Voici quelques exemples des classes avec lesquelles je travaille:

Modèle de paramètres / options

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 assembly 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);
        }
    }
}
Mat
la source
1
Pourriez-vous fournir un petit exemple de code du bloc que vous essayez de vous moquer? Merci!
AJ X.29
Vous confondez le sens de la moquerie? Vous vous moquez d'une interface et la configurez pour renvoyer une valeur spécifiée. Car IOptions<T>vous n'avez qu'à vous moquer Valuepour rendre le cours que vous désirez
Tseng

Réponses:

253

Vous devez créer et remplir manuellement un IOptions<SampleOptions>objet. Vous pouvez le faire via la Microsoft.Extensions.Options.Optionsclasse helper. Par exemple:

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

Vous pouvez simplifier un peu cela pour:

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

De toute évidence, ce n'est pas très utile en l'état. Vous devrez créer et remplir un objet SampleOptions et le transmettre à la méthode Create.

Necoras
la source
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 certainement celle que j'utilise. Et cela fonctionne très bien!
grahamesd le
Très bonne réponse. Beaucoup plus simple que de s'appuyer sur un cadre moqueur.
Chris Lawrence
2
Merci. J'étais tellement fatigué d'écrire new OptionsWrapper<SampleOptions>(new SampleOptions());partout
BritishDeveloper
59

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 restaurée, l'utilisation du framework MOQ est aussi simple que de créer une instance de la classe SampleOptions, puis, comme mentionné, de l'affecter à Value.

Voici un aperçu du code à quoi cela 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 la maquette configurée, vous pouvez maintenant transmettre l'objet maquette au constructeur en tant que

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

Pour info, j'ai un référentiel git qui décrit ces 2 approches sur Github / patvin80

patvin80
la source
Cela devrait être la réponse acceptée, cela fonctionne parfaitement.
alessandrocb
J'aimerais vraiment que cela fonctionne pour moi, mais ce n'est pas le cas: (Moq 4.13.1
kanpeki
21

Vous pouvez éviter d'utiliser MOQ du tout. Utilisez dans votre fichier de configuration de tests .json. Un fichier pour de nombreux fichiers de classe de test. Ce sera bien d'utiliser ConfigurationBuilderdans ce cas.

Exemple de appsetting.json

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

Exemple de classe de mappage des paramètres:

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

Exemple de service nécessaire pour 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);
    }
}
Aleha
la source
Cela a bien fonctionné pour moi, bravo! 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.
Harry
3
Fonctionne très bien, mais l'information essentielle manquante est que vous devez inclure le package nuget Microsoft.Extensions.Configuration.Binder, sinon vous n'obtiendrez pas la méthode d'extension "Get <SomeServiceConfiguration>" disponible.
Cinétique
J'ai dû exécuter dotnet add package Microsoft.Extensions.Configuration.Json pour que cela fonctionne. Très bonne réponse!
Leonardo Wildt
1
J'ai également dû modifier les propriétés du fichier appsettings.json pour utiliser le fichier dans le fichier bin, car Directory.GetCurrentDirectory () renvoyait le contenu du fichier bin. Dans "Copier dans le répertoire de sortie" de appsettings.json, j'ai défini la valeur sur "Copier si plus récent".
bpz le
14

Classe donnée Personqui dépend de ce PersonSettingsqui 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 être moqués et Personpeuvent être testés 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>dans Personau 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");
    }
}
Frank Rem
la source
Vous ne testez rien d'utile. Le framework pour DI my Microsoft est déjà testé à l'unité. Dans l'état actuel des choses, il s'agit en fait d'un test d'intégration (intégration avec un framework tiers).
Erik Philips
2
@ErikPhilips Mon code montre comment se moquer des IOptions <T> comme demandé par l'OP. Je suis d'accord qu'il ne teste rien d'utile en soi, mais cela peut être utile de tester autre chose.
Frank Rem
13

Vous pouvez toujours créer vos options via Options.Create () et utiliser simplement AutoMocker.Use (options) avant de créer l'instance simulée du référentiel que vous testez. L'utilisation d'AutoMocker.CreateInstance <> () facilite la création d'instances sans passer manuellement les paramètres

J'ai un peu changé votre SampleRepo afin de pouvoir reproduire le comportement que je pense que vous souhaitez obtenir.

public class SampleRepoTests
{
    private readonly AutoMocker _mocker = new AutoMocker();
    private readonly ISampleRepo _sampleRepo;

    private readonly IOptions<SampleOptions> _options = Options.Create(new SampleOptions()
        {FirstSetting = "firstSetting"});

    public SampleRepoTests()
    {
        _mocker.Use(_options);
        _sampleRepo = _mocker.CreateInstance<SampleRepo>();
    }

    [Fact]
    public void Test_Options_Injected()
    {
        var firstSetting = _sampleRepo.GetFirstSetting();
        Assert.True(firstSetting == "firstSetting");
    }
}

public class SampleRepo : ISampleRepo
{
    private SampleOptions _options;

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

    public string GetFirstSetting()
    {
        return _options.FirstSetting;
    }
}

public interface ISampleRepo
{
    string GetFirstSetting();
}

public class SampleOptions
{
    public string FirstSetting { get; set; }
}
Matei
la source
8

Voici un autre moyen simple qui n'a pas besoin de Mock, mais utilise à la place le 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);
Robert Corvus
la source
2

Pour mes tests système et d'intégration, je préfère avoir une copie / un lien de mon fichier de configuration dans le projet de test. Et puis j'utilise ConfigurationBuilder pour obtenir les options.

using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace SomeProject.Test
{
public static class TestEnvironment
{
    private static object configLock = new object();

    public static ServiceProvider ServiceProvider { get; private set; }
    public static T GetOption<T>()
    {
        lock (configLock)
        {
            if (ServiceProvider != null) return (T)ServiceProvider.GetServices(typeof(T)).First();

            var builder = new ConfigurationBuilder()
                .AddJsonFile("config/appsettings.json", optional: false, reloadOnChange: true)
                .AddEnvironmentVariables();
            var configuration = builder.Build();
            var services = new ServiceCollection();
            services.AddOptions();

            services.Configure<ProductOptions>(configuration.GetSection("Products"));
            services.Configure<MonitoringOptions>(configuration.GetSection("Monitoring"));
            services.Configure<WcfServiceOptions>(configuration.GetSection("Services"));
            ServiceProvider = services.BuildServiceProvider();
            return (T)ServiceProvider.GetServices(typeof(T)).First();
        }
    }
}
}

De cette façon, je peux utiliser la configuration partout dans mon TestProject. Pour les tests unitaires, je préfère utiliser MOQ comme décrit par patvin80.

Mithrandir
la source
1

D'accord avec Aleha, l'utilisation d'un fichier de configuration testSettings.json est probablement meilleure. Et puis, au lieu d'injecter l'IOption, vous pouvez simplement injecter les vrais SampleOptions dans votre constructeur de classe, lorsque vous testez la classe unitaire, vous pouvez faire ce qui suit dans un fixture ou à nouveau simplement dans le constructeur de la classe de test:

   var builder = new ConfigurationBuilder()
  .AddJsonFile("testSettings.json", true, true)
  .AddEnvironmentVariables();

  var configurationRoot = builder.Build();
  configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
BobTheOtherBuilder
la source