c# - test - Prueba de unidad de núcleo.Net-IOptions simuladas<T>
unit test dotnet (8)
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);
}
}
}
Aquí hay otra manera 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);
Clase dada
Person
que depende de
PersonSettings
siguiente manera:
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 la
Person
se puede probar 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
Person
en lugar 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");
}
}
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);
Para mis pruebas de sistema y de 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.
Puede evitar usar MOQ en absoluto.
Úselo en sus pruebas .json archivo de configuración.
Un archivo para muchos archivos de clase de prueba.
Estará bien usar
ConfigurationBuilder
en 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);
}
}
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
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; }
}
IOptions<SampleOptions>
crear y completar manualmente un objeto
IOptions<SampleOptions>
.
Puede hacerlo a través de la clase auxiliar
Microsoft.Extensions.Options.Options
.
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.