Teste de unidade de núcleo .Net - Mock IOptions <T>

137

Sinto que estou perdendo algo realmente óbvio aqui. Eu tenho classes que exigem a injeção de opções usando o padrão .Net Core IOptions (?). Quando vou ao teste de unidade dessa classe, quero zombar de várias versões das opções para validar a funcionalidade da classe. Alguém sabe como simular / instanciar / preencher corretamente IOptions fora da classe Startup?

Aqui estão alguns exemplos das classes com as quais estou trabalhando:

Modelo de configurações / opções

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 a ser testada que usa as Configurações:

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

Teste de unidade em uma montagem diferente das outras 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);
        }
    }
}
Matt
fonte
1
Você poderia fornecer um pequeno exemplo de código do bloco que você está tentando zombar? obrigado!
AJ X.
Você está confundindo o significado de zombar? Você zomba de uma interface e a configura para retornar um valor especificado. Pois IOptions<T>você só precisa zombar Valuepara retornar a classe que deseja
Tseng 29/11

Respostas:

253

Você precisa criar e preencher manualmente um IOptions<SampleOptions>objeto. Você pode fazer isso através da Microsoft.Extensions.Options.Optionsclasse auxiliar. Por exemplo:

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

Você pode simplificar um pouco para:

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

Obviamente, isso não é muito útil como é. Você realmente precisará criar e preencher um objeto SampleOptions e passá-lo para o método Create.

Necoras
fonte
Aprecio todas as respostas adicionais que mostram como usar o Moq etc., mas essa resposta é tão simples que é definitivamente a que estou usando. E funciona muito bem!
grahamesd
Ótima resposta. Muito mais simples do que contar com uma estrutura de zombaria.
Chris Lawrence
2
obrigado. Eu estava tão cansado de escrever em new OptionsWrapper<SampleOptions>(new SampleOptions());todos os lugares
BritishDeveloper 17/07
59

Se você pretende usar o Mocking Framework conforme indicado por @TSeng no comentário, precisará adicionar a seguinte dependência no seu arquivo project.json.

   "Moq": "4.6.38-alpha",

Depois que a dependência é restaurada, o uso da estrutura MOQ é tão simples quanto criar uma instância da classe SampleOptions e, como mencionado, atribuí-la ao Valor.

Aqui está um resumo de código como seria.

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

Depois que a simulação é configurada, agora você pode passar o objeto de simulação para o contratante como

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

Para sua informação, eu tenho um repositório git que descreve essas duas abordagens no Github / patvin80

patvin80
fonte
Esta deve ser a resposta aceita, funciona perfeitamente.
Alessandrocb
Realmente gostaria que isso funcionasse para mim, mas não funciona :( Moq 4.13.1
kanpeki
21

Você pode evitar o uso de MOQ. Use em seu arquivo de configuração .json de testes. Um arquivo para muitos arquivos de classe de teste. Vai ser bom usar ConfigurationBuilderneste caso.

Exemplo de appsetting.json

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

Exemplo de classe de mapeamento de configurações:

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

Exemplo de serviço necessário para testar:

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

Classe de teste 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
fonte
Isso funcionou muito bem para mim, saúde! Não queria usar o Moq para algo que parecia tão simples e não queria preencher minhas próprias opções com as configurações.
Harry
3
Funciona muito bem, mas a parte essencial da informação que falta é que você precisa incluir o pacote de nuget Microsoft.Extensions.Configuration.Binder, caso contrário você não obterá o método de extensão "Get <SomeServiceConfiguration>" disponível.
21418 Kinetic
Eu tive que executar o dotnet add package Microsoft.Extensions.Configuration.Json para que isso funcionasse. Ótima resposta!
Leonardo Wildt
1
Também tive que alterar as propriedades do arquivo appsettings.json para usar o arquivo no arquivo bin, pois Directory.GetCurrentDirectory () estava retornando o conteúdo do arquivo bin. Em "Copiar para o diretório de saída" do appsettings.json, defino o valor como "Copiar se for mais recente".
BPZ
14

Classe dada Personque depende do PersonSettingsseguinte:

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>pode ser ridicularizado e Personpode ser testado da seguinte maneira:

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

Para injetar IOptions<PersonSettings>em Personvez de passá-lo explicitamente para o ctor, use este código:

[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
fonte
Você não está testando nada de útil. A estrutura para o DI my Microsoft já está testada em unidade. Tal como está, este é realmente um teste de integração (integração com uma estrutura de terceiros).
Erik Philips
2
@ErikPhilips Meu código mostra como simular IOptions <T> conforme solicitado pelo OP. Concordo que ele não testa nada de útil por si só, mas pode ser útil testar outra coisa.
Frank Rem
13

Você sempre pode criar suas opções via Options.Create () e simplesmente usar o AutoMocker.Use (options) antes de criar a instância simulada do repositório que está testando. O uso do AutoMocker.CreateInstance <> () facilita a criação de instâncias sem passar manualmente os parâmetros

Eu mudei um pouco o SampleRepo para poder reproduzir o comportamento que acho que você deseja alcançar.

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
fonte
8

Aqui está outra maneira fácil que não precisa do Mock, mas usa o 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
fonte
2

Para meus testes de sistema e integração, prefiro ter uma cópia / link do meu arquivo de configuração dentro do projeto de teste. E então eu uso o ConfigurationBuilder para obter as opções.

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

Dessa forma, eu posso usar a configuração em qualquer lugar dentro do meu TestProject. Para testes de unidade, prefiro usar o MOQ como o patvin80 descrito.

Mithrandir
fonte
1

Concorde com Aleha que o uso de um arquivo de configuração testSettings.json provavelmente é melhor. E então, em vez de injetar a IOption, você pode simplesmente injetar as SampleOptions reais no construtor de sua classe. Ao testar a classe por unidade, você pode fazer o seguinte em um equipamento ou novamente apenas no construtor da classe de teste:

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

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