Prueba de unidad de núcleo .Net - IOptions simuladas

137

Siento que me estoy perdiendo algo realmente obvio aquí. Tengo clases que requieren la inyección de opciones usando el patrón .Net Core IOptions (?). Cuando voy a la prueba unitaria de esa clase, quiero burlarme de varias versiones de las opciones para validar la funcionalidad de la clase. ¿Alguien sabe cómo burlarse / instanciarse / poblar correctamente IOptions fuera de la clase de inicio?

Aquí hay algunas muestras de las clases con las que estoy trabajando:

Configuraciones / Opciones Modelo

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

Clase a probar que utiliza la configuración:

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

Prueba unitaria en un ensamblaje diferente de las otras clases:

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);
        }
    }
}
Mate
fuente
1
¿Podría proporcionar un pequeño código de ejemplo del bloque que está intentando burlarse? ¡Gracias!
AJ X.
¿Estás confundiendo el significado de burlarse? Se burla de una interfaz y la configura para devolver un valor especificado. Para IOptions<T>que solo tengas que burlarte Valuede devolver la clase que deseas
Tseng

Respuestas:

253

Necesita crear y rellenar manualmente un IOptions<SampleOptions>objeto. Puede hacerlo a través de la Microsoft.Extensions.Options.Optionsclase auxiliar. Por ejemplo:

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

Puede simplificar eso un poco para:

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

Obviamente esto no es muy útil como es. Deberá crear y completar un objeto SampleOptions y pasarlo al método Create.

Necoras
fuente
Aprecio todas las respuestas adicionales que muestran cómo usar Moq, etc., pero esta respuesta es tan simple que definitivamente es la que estoy usando. ¡Y funciona muy bien!
Grahamesd
Gran respuesta. Mucho más simple que confiar en un marco burlón.
Chris Lawrence
2
Gracias. Estaba tan cansado de escribir en new OptionsWrapper<SampleOptions>(new SampleOptions());todas partes
BritishDeveloper
59

Si tiene la intención de utilizar Mocking Framework como lo indica @TSeng en el comentario, debe agregar la siguiente dependencia en su archivo project.json.

   "Moq": "4.6.38-alpha",

Una vez que se restaura la dependencia, usar el marco MOQ es tan simple como crear una instancia de la clase SampleOptions y luego, como se mencionó, asignarla al Valor.

Aquí hay un resumen del código de cómo se vería.

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 vez que el simulacro está configurado, ahora puede pasar el objeto simulado al contructor como

SampleRepo sr = new SampleRepo(mock.Object);   

HTH

Para su información, tengo un repositorio git que describe estos 2 enfoques en Github / patvin80

patvin80
fuente
Esta debería ser la respuesta aceptada, funciona perfectamente.
alessandrocb
Realmente desearía que esto funcionara para mí, pero no funciona :( Moq 4.13.1
kanpeki
21

Puede evitar usar MOQ en absoluto. Úselo en su archivo de configuración de pruebas .json. Un archivo para muchos archivos de clase de prueba. Estará bien usarlo ConfigurationBuilderen este caso.

Ejemplo de appsetting.json

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

Ejemplo de clase de mapeo de configuraciones:

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

Ejemplo de servicio que se necesita para probar:

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

Clase de prueba 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
fuente
Esto funcionó muy bien para mí, ¡salud! No quería usar Moq para algo que parecía tan simple y no quería intentar llenar mis propias opciones con los ajustes de configuración.
Harry
3
Funciona muy bien, pero la información vital que falta es que debe incluir el paquete Microsoft.Extensions.Configuration.Binder nuget, de lo contrario no obtendrá el método de extensión "Get <SomeServiceConfiguration>" disponible.
Cinética
Tuve que ejecutar dotnet add package Microsoft.Extensions.Configuration.Json para que esto funcione. ¡Gran respuesta!
Leonardo Wildt
1
También tuve que cambiar las propiedades del archivo appsettings.json para usar el archivo en el archivo bin, ya que Directory.GetCurrentDirectory () estaba devolviendo el contenido del archivo bin. En "Copiar al directorio de salida" de appsettings.json, configuro el valor en "Copiar si es más reciente".
bpz
14

Clase dada Personque depende de PersonSettingslo siguiente:

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>se puede burlar y Personprobar de la siguiente manera:

[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 inyectar IOptions<PersonSettings>en Personlugar de pasarlo explícitamente al 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
fuente
No estás probando nada útil. El marco para DI my Microsoft ya está probado en la unidad. Tal como está, esta es realmente una prueba de integración (integración con un marco de terceros).
Erik Philips
2
@ErikPhilips Mi código muestra cómo burlarse de IOptions <T> según lo solicitado por el OP. Estoy de acuerdo en que no prueba nada útil en sí mismo, pero puede ser útil probar otra cosa.
Frank Rem
13

Siempre puede crear sus opciones a través de Options.Create () y luego simplemente usar AutoMocker.Use (options) antes de crear la instancia simulada del repositorio que está probando. El uso de AutoMocker.CreateInstance <> () facilita la creación de instancias sin pasar parámetros manualmente

He cambiado un poco tu SampleRepo para poder reproducir el comportamiento que creo que quieres lograr.

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

Aquí hay otra forma fácil que no necesita Mock, sino que usa el 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
fuente
2

Para mis pruebas de sistema e integración, prefiero tener una copia / enlace de mi archivo de configuración dentro del proyecto de prueba. Y luego uso el ConfigurationBuilder para obtener las opciones.

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 esta manera puedo usar la configuración en todas partes dentro de mi TestProject. Para las pruebas unitarias, prefiero usar MOQ como se describe en patvin80.

Mithrandir
fuente
1

De acuerdo con Aleha en que usar un archivo de configuración testSettings.json es probablemente mejor. Y luego, en lugar de inyectar la IOption, simplemente puede inyectar las SampleOptions reales en el constructor de su clase, cuando la unidad pruebe la clase, puede hacer lo siguiente en un dispositivo fijo o de nuevo solo en el constructor de la clase de prueba:

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

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