.NET Core Unit Testing - Mock IOptions <T>

150

Czuję, że brakuje mi czegoś naprawdę oczywistego. Mam klasy, które wymagają wstrzykiwania opcji przy użyciu IOptionswzorca .NET Core (?). Kiedy przeprowadzam testy jednostkowe tej klasy, chcę mockować różne wersje opcji, aby zweryfikować funkcjonalność klasy. Czy ktoś wie, jak poprawnie mockować / tworzyć wystąpienia / wypełniać IOptions<T>poza klasą Startup?

Oto kilka przykładów zajęć, z którymi pracuję:

Ustawienia / Opcje Model

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

Klasa do przetestowania korzystająca z Ustawień:

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 jednostkowy w innym zestawie z innych klas:

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
źródło
Czy mylisz znaczenie kpiny? Naśladujesz interfejs i konfigurujesz go tak, aby zwracał określoną wartość. Dla IOptions<T>trzeba tylko drwić Valuewrócić klasę pragnienie
Tseng

Odpowiedzi:

279

Musisz ręcznie utworzyć i wypełnić IOptions<SampleOptions>obiekt. Możesz to zrobić za pośrednictwem Microsoft.Extensions.Options.Optionsklasy pomocnika. Na przykład:

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

Możesz to nieco uprościć, aby:

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

Oczywiście nie jest to zbyt przydatne, tak jak jest. Będziesz musiał faktycznie utworzyć i zapełnić obiekt SampleOptions i przekazać go do metody Create.

Necoras
źródło
Doceniam wszystkie dodatkowe odpowiedzi, które pokazują, jak używać Moq itp., Ale ta odpowiedź jest tak prosta, że ​​zdecydowanie jest tą, której używam. I działa świetnie!
grahamesd
Świetna odpowiedź. O wiele prostsze niż poleganie na kpiącym frameworku.
Chris Lawrence
2
dzięki. Byłem tak zmęczony pisaniem new OptionsWrapper<SampleOptions>(new SampleOptions());wszędzie
BritishDeveloper
64

Jeśli zamierzasz używać Mocking Framework, jak wskazano przez @TSeng w komentarzu, musisz dodać następującą zależność w pliku project.json.

   "Moq": "4.6.38-alpha",

Po przywróceniu zależności użycie struktury MOQ jest tak proste, jak utworzenie wystąpienia klasy SampleOptions, a następnie, jak wspomniano, przypisanie jej do wartości.

Oto zarys kodu, jak by to wyglądało.

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

Po skonfigurowaniu makiety możesz teraz przekazać obiekt makiety konstruktorowi jako plik

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

FYI Mam repozytorium git, które przedstawia te 2 podejścia na Github / patvin80

patvin80
źródło
1
To powinna być akceptowana odpowiedź, działa idealnie.
alessandrocb
Naprawdę chciałbym, żeby to zadziałało, ale tak nie jest :( Moq 4.13.1
kanpeki
23

Możesz w ogóle uniknąć używania MOQ. Użyj w swoim pliku konfiguracyjnym .json. Jeden plik na wiele plików klas testowych. W ConfigurationBuildertym przypadku będzie dobrze używać .

Przykład pliku appsetting.json

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

Przykład klasy odwzorowania ustawień:

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

Przykład usługi potrzebnej do przetestowania:

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

Klasa testu 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
źródło
U mnie to dobrze zadziałało, na zdrowie! Nie chciałem używać Moq do czegoś, co wydawało się tak proste i nie chciałem próbować wypełniać moich własnych opcji ustawieniami konfiguracyjnymi.
Harry
4
Działa świetnie, ale kluczową brakującą informacją jest to, że musisz dołączyć pakiet Microsoft.Extensions.Configuration.Binder nuget, w przeciwnym razie nie otrzymasz dostępnej metody rozszerzenia „Get <SomeServiceConfiguration>”.
Kinetic
Musiałem uruchomić pakiet dotnet add Microsoft.Extensions.Configuration.Json, aby to zadziałało. Świetna odpowiedź!
Leonardo Wildt
1
Musiałem również zmienić właściwości pliku appsettings.json, aby użyć pliku w pliku bin, ponieważ Directory.GetCurrentDirectory () zwracał zawartość pliku bin. W „Kopiuj do katalogu wyjściowego” pliku appsettings.json ustawiłem wartość na „Kopiuj, jeśli nowszy”.
bpz
15

Zawsze możesz utworzyć swoje opcje za pomocą Options.Create (), a następnie po prostu użyć AutoMocker.Use (options), zanim faktycznie utworzysz symulowaną instancję repozytorium, które testujesz. Korzystanie z AutoMocker.CreateInstance <> () ułatwia tworzenie instancji bez ręcznego przekazywania parametrów

Zmieniłem trochę SampleRepo, aby móc odtworzyć zachowanie, które myślę, że chcesz osiągnąć.

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
źródło
14

Dana klasa Personzależy od 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>można wyśmiać i Personprzetestować w następujący sposób:

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

Aby wstrzyknąć IOptions<PersonSettings>do Personzamiast przekazywania go jawnie do ctora, użyj tego kodu:

[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");
    }
}
Szczery
źródło
Nie testujesz niczego przydatnego. Framework dla DI my Microsoft jest już przetestowany jednostkowo. W obecnej formie jest to tak naprawdę test integracji (integracja z platformą innej firmy).
Erik Philips
3
@ErikPhilips Mój kod pokazuje, jak mockować IOptions <T> zgodnie z żądaniem OP. Zgadzam się, że sam w sobie nie testuje niczego pożytecznego, ale może być przydatny do testowania czegoś innego.
Frank
8

Oto kolejny łatwy sposób, który nie wymaga Mocka, ale zamiast tego używa 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
źródło
2

W przypadku testów systemu i testów integracyjnych wolę mieć kopię / link do mojego pliku konfiguracyjnego w projekcie testowym. Następnie używam ConfigurationBuilder, aby uzyskać opcje.

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

W ten sposób mogę używać konfiguracji wszędzie w moim TestProject. W przypadku testów jednostkowych wolę używać MOQ, jak opisano w patvin80.

Mithrandir
źródło
1

Zgadzam się z Aleha, że ​​użycie pliku konfiguracyjnego testSettings.json jest prawdopodobnie lepsze.

A następnie, zamiast wstrzykiwać IOption <SampleOptions>, możesz po prostu wstrzyknąć prawdziwe SampleOptions do konstruktora klasy, podczas testów jednostkowych klasy możesz wykonać następujące czynności w urządzeniu lub ponownie w konstruktorze klasy testowej:

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

var configurationRoot = builder.Build();
configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
BobTheOtherBuilder
źródło