Czuję, że brakuje mi czegoś naprawdę oczywistego. Mam klasy, które wymagają wstrzykiwania opcji przy użyciu IOptions
wzorca .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);
}
}
}
IOptions<T>
trzeba tylko drwićValue
wrócić klasę pragnienieOdpowiedzi:
Musisz ręcznie utworzyć i wypełnić
IOptions<SampleOptions>
obiekt. Możesz to zrobić za pośrednictwemMicrosoft.Extensions.Options.Options
klasy 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.
źródło
new OptionsWrapper<SampleOptions>(new SampleOptions());
wszędzieJeś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
źródło
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
ConfigurationBuilder
tym 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); } }
źródło
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; } }
źródło
Dana klasa
Person
zależy odPersonSettings
: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ć iPerson
przetestować 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>
doPerson
zamiast 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"); } }
źródło
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);
źródło
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.
źródło
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);
źródło