.Net Тестування основних блоків - макет IOptions <T>


137

Я відчуваю, що тут пропускаю щось дійсно очевидне. У мене є класи, які вимагають введення параметрів за допомогою шаблону .Net Core IOptions (?). Коли я переходжу до тестової одиниці цього класу, я хочу знущатися над різними версіями щодо варіантів перевірки функціональності класу. Хтось знає, як правильно знущатися / інстанціювати / заповнювати IOptions поза класом Startup?

Ось кілька зразків класів, з якими я працюю:

Модель налаштувань / параметрів

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

Клас для тестування, який використовує Налаштування:

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

Тест одиниці в іншому складі, ніж інші класи:

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
Чи можете ви надати невеликий приклад коду блоку, який ви намагаєтеся знущатися? Дякую!
AJ X.

Ви плутаєте значення глузування? Ви знущаєтесь над інтерфейсом і налаштовуєте його для повернення заданого значення. Для IOptions<T>вас є тільки знущатися Valueповернути клас ви хочете
Цзен

Відповіді:


253

Вам потрібно вручну створити та заповнити IOptions<SampleOptions>об’єкт. Це можна зробити через Microsoft.Extensions.Options.Optionsклас помічників. Наприклад:

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

Ви можете трохи спростити це:

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

Очевидно, це не дуже корисно, як є. Вам потрібно буде фактично створити та заповнити об’єкт SampleOptions і передати його в метод Create.


Я ціную всі додаткові відповіді, які показують, як користуватися Moq тощо, але ця відповідь настільки проста, що, безумовно, є той, який я використовую. І це чудово працює!
grahamesd

Чудова відповідь. Набагато простіше, ніж покладатися на глузливі рамки.
Кріс Лоуренс

2
Дякую. Мені так набридло писати new OptionsWrapper<SampleOptions>(new SampleOptions());всюди
BritishDeveloper

59

Якщо ви маєте намір використовувати Mocking Framework, як зазначено в коментарі @TSeng, вам потрібно додати наступну залежність у файл project.json.

   "Moq": "4.6.38-alpha",

Як тільки залежність відновлена, використовувати рамку MOQ так само просто, як створити екземпляр класу SampleOptions, а потім, як згадується, призначити його Значення.

Ось контур, як це виглядатиме.

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

Після налаштування макету тепер ви можете передати макетний об’єкт до кондуктора як

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

FYI У мене є сховище git, яке окреслює ці два підходи щодо Github / patvin80


Це має бути прийнята відповідь, вона працює чудово.
alessandrocb

Дуже бажаю, щоб це працювало для мене, але це не так :(
Moq 4.13.1

21

Ви взагалі можете уникати використання MOQ. Використовуйте у своєму тестовому файлі конфігурації .json. Один файл для багатьох файлів тестового класу. В ConfigurationBuilderцьому випадку буде добре використовувати .

Приклад appsetting.json

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

Приклад класу картографічного налаштування:

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

Приклад обслуговування, необхідне для тестування:

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

Тестовий клас 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);
    }
}

Це добре працювало для мене, ура! Я не хотів використовувати Moq для чогось, що здалося таким простим, і не хотів намагатися заповнити власні параметри налаштуваннями конфігурації.
Гаррі

3
Чудово працює, але найважливішою інформацією про відсутність є те, що вам потрібно включити нульовий пакет Microsoft.Extensions.Configuration.Binder, інакше ви не отримаєте доступного методу розширення "Get <SomeServiceConfiguration>".
Кінетичний

Мені довелося запустити пакунок dotnet add Microsoft.Extensions.Configuration.Json, щоб змусити це працювати. Чудова відповідь!
Леонардо Уайлдт

1
Я також повинен був змінити властивості файлу appsettings.json, щоб використовувати файл у файлі bin, оскільки Directory.GetCurrentDirectory () повертав вміст файлу bin. У "Копіювати у вихідний каталог" appsettings.json я встановлюю значення "Копіювати, якщо новіше".
bpz

14

Даний клас, Personякий залежить від PersonSettingsнаступного:

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>можна знущатися і Personможе бути перевірена наступним чином:

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

Для того, щоб внести IOptions<PersonSettings>в Personнього замість того, щоб явно передати його в ctor, використовуйте цей код:

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

Ви не тестуєте нічого корисного. Рамки для DI мого Microsoft вже перевірені. На даний момент це справді інтеграційний тест (інтеграція із сторонніми рамками).
Ерік Філіпс

2
@ErikPhilips Мій код показує, як знущатися з IOptions <T> відповідно до вимог ОП. Я погоджуюся, що це не тестує нічого корисного саме по собі, але може бути корисним тестувати щось інше.
Френк Рем

13

Ви завжди можете створити свої параметри за допомогою Options.Create () і, ніж просто, використовувати AutoMocker.Use (options), перш ніж створити насмішкований екземпляр тестового сховища. Використання AutoMocker.CreateInstance <> () полегшує створення примірників без передачі параметрів вручну

Я трохи змінив ти SampleRepo, щоб можна було відтворити поведінку, яку я думаю, що ти хочеш досягти.

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

Ось ще один простий спосіб, який не потребує Mock, але натомість використовує 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

Для моїх системних та інтеграційних тестів я вважаю за краще мати копію / посилання мого конфігураційного файлу всередині тестового проекту. І тоді я використовую ConfigurationBuilder, щоб отримати варіанти.

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

Таким чином я можу використовувати конфігу скрізь всередині свого TestProject. Для одиничних тестів я вважаю за краще використовувати описаний MOQ, як описаний patvin80.


1

Погодьтеся з Aleha, що використання файлу конфігурації testSettings.json, ймовірно, краще. І тоді замість того, щоб вводити IOption, ви можете просто ввести реальні SampleOptions у конструктор свого класу, коли блок тестує клас, ви можете зробити наступне у кріпленні або знову просто у конструкторі тестового класу:

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

  var configurationRoot = builder.Build();
  configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.