.Net Core Unit Testing - Mock IOptions <T>


137

Sento che mi manca qualcosa di veramente ovvio qui. Ho delle classi che richiedono l'iniezione di opzioni usando il modello IONet Core IOptions (?). Quando vado al test unitario di quella classe, voglio prendere in giro varie versioni delle opzioni per convalidare la funzionalità della classe. Qualcuno sa come deridere / istanziare / popolare correttamente IOptions al di fuori della classe Startup?

Ecco alcuni esempi delle classi con cui sto lavorando:

Modello impostazioni / opzioni

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 da testare che utilizza le Impostazioni:

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 unitario in un assieme diverso dalle altre classi:

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

1
Potresti fornire un piccolo esempio di codice del blocco che stai cercando di deridere? Grazie!
AJ X.

Stai confondendo il significato del deridere? Si deride su un'interfaccia e la si configura per restituire un valore specificato. Perché IOptions<T>devi solo deridere Valueper restituire la lezione che desideri
Tseng,

Risposte:


253

È necessario creare e popolare manualmente un IOptions<SampleOptions>oggetto. Puoi farlo tramite la Microsoft.Extensions.Options.Optionsclasse helper. Per esempio:

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

Puoi semplificarlo un po 'per:

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

Ovviamente questo non è molto utile così com'è. Dovrai effettivamente creare e popolare un oggetto SampleOptions e passarlo nel metodo Create.


Apprezzo tutte le risposte aggiuntive che mostrano come usare Moq, ecc., Ma questa risposta è così semplice che è sicuramente quella che sto usando. E funziona benissimo!
Grahamesd,

Bella risposta. Molto più semplice che fare affidamento su un quadro beffardo.
Chris Lawrence,

2
Grazie. Ero così stanco di scrivere new OptionsWrapper<SampleOptions>(new SampleOptions());ovunque
British Devevoper il

59

Se si intende utilizzare Mocking Framework come indicato da @TSeng nel commento, è necessario aggiungere la seguente dipendenza nel file project.json.

   "Moq": "4.6.38-alpha",

Una volta ripristinata la dipendenza, l'utilizzo del framework MOQ è semplice come creare un'istanza della classe SampleOptions e, come detto, assegnarla al valore.

Ecco una descrizione del codice che aspetto avrebbe.

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

Una volta impostato il finto, ora è possibile passare l'oggetto simulato al contructor come

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

Cordiali saluti, ho un repository git che delinea questi 2 approcci su Github / patvin80


Questa dovrebbe essere la risposta accettata, funziona perfettamente.
alessandrocb,

Vorrei davvero che funzionasse per me, ma non lo fa :( Moq 4.13.1
kanpeki,

21

Puoi evitare di usare MOQ. Utilizzare nei file di configurazione di test .json. Un file per molti file di classe di test. Andrà bene ConfigurationBuilderin questo caso.

Esempio di appsetting.json

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

Esempio di classe di mappatura delle impostazioni:

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

Esempio di servizio necessario per testare:

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

Classe di prova 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);
    }
}

Questo ha funzionato bene per me, evviva! Non volevo usare Moq per qualcosa che sembrava così semplice e non volevo provare a popolare le mie opzioni con le impostazioni di configurazione.
Harry

3
Funziona alla grande, ma l'informazione essenziale mancante è che è necessario includere il pacchetto nuget Microsoft.Extensions.Configuration.Binder, altrimenti non è disponibile il metodo di estensione "Get <SomeServiceConfiguration>".
Kinetic,

Ho dovuto eseguire dotnet aggiungere il pacchetto Microsoft.Extensions.Configuration.Json per farlo funzionare. Bella risposta!
Leonardo Wildt,

1
Ho anche dovuto modificare le proprietà del file appsettings.json per utilizzare il file nel file bin, poiché Directory.GetCurrentDirectory () restituiva il contenuto del file bin. In "Copia nella directory di output" di appsettings.json, ho impostato il valore su "Copia se più recente".
BP

14

Data classe Personche dipende PersonSettingscome segue:

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>può essere deriso e Personpuò essere testato come segue:

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

Per iniettare IOptions<PersonSettings>nella Personinvece di passarlo in modo esplicito alla ctor, utilizzare questo codice:

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

Non stai testando nulla di utile. Il framework per DI my Microsoft è già testato in unità. Allo stato attuale questo è davvero un test di integrazione (integrazione con un framework di terze parti).
Erik Philips,

2
@ErikPhilips Il mio codice mostra come deridere IOptions <T> come richiesto dall'OP. Concordo sul fatto che non testa nulla di utile in sé, ma può essere utile testare qualcos'altro.
Frank Rem,

13

Puoi sempre creare le tue opzioni tramite Options.Create () e utilizzare semplicemente AutoMocker.Use (opzioni) prima di creare l'istanza beffarda del repository che stai testando. L'uso di AutoMocker.CreateInstance <> () semplifica la creazione di istanze senza passare manualmente i parametri

Ho cambiato un po 'il tuo SampleRepo per poter riprodurre il comportamento che penso tu voglia ottenere.

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

8

Ecco un altro modo semplice che non ha bisogno di Mock, ma utilizza invece 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);

2

Per i miei test di sistema e integrazione preferisco avere una copia / collegamento del mio file di configurazione all'interno del progetto di test. E poi uso ConfigurationBuilder per ottenere le opzioni.

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

In questo modo posso usare la configurazione ovunque all'interno del mio TestProject. Per i test unitari preferisco usare MOQ come descritto da patvin80.


1

Concordo con Aleha sul fatto che utilizzare un file di configurazione testSettings.json sia probabilmente migliore. E poi invece di iniettare l'IOption puoi semplicemente iniettare le SampleOptions reali nel costruttore della tua classe, quando l'unità verifica la classe, puoi fare quanto segue in un dispositivo o ancora solo nel costruttore della classe test:

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

  var configurationRoot = builder.Build();
  configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.