.NET Core Unit Testing - Mock IOptions

前端 未结 8 848
难免孤独
难免孤独 2020-12-12 17:55

I feel like I\'m missing something really obvious here. I have classes that require injecting of options using the .NET Core IOptions pattern(?). When I unit te

相关标签:
8条回答
  • 2020-12-12 18:07

    Given class Person that depends on PersonSettings as follows:

    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> can be mocked and Person can be tested as follows:

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

    To inject IOptions<PersonSettings> into Person instead of passing it explicitly to the ctor, use this code:

    [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");
        }
    }
    
    0 讨论(0)
  • 2020-12-12 18:15

    Here's another easy way that doesn't need Mock, but instead uses the 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);
    
    0 讨论(0)
  • 2020-12-12 18:15

    For my system and integration tests I prefer to have a copy/link of my config file inside the test project. And then I use the ConfigurationBuilder to get the options.

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

    This way I can use the config everywhere inside of my TestProject. For unit tests I prefer to use MOQ like patvin80 described.

    0 讨论(0)
  • 2020-12-12 18:19

    You can avoid using MOQ at all. Use in your tests .json configuration file. One file for many test class files. It will be fine to use ConfigurationBuilder in this case.

    Example of appsetting.json

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

    Example of settings mapping class:

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

    Example of service which is needed to test:

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

    NUnit test class:

    [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);
        }
    }
    
    0 讨论(0)
  • 2020-12-12 18:20

    Agree with Aleha that using a testSettings.json configuration file is probably better.

    And then, instead of injecting the IOption<SampleOptions>, you can simply inject the real SampleOptions in your class constructor, when unit test the class, you can do the following in a fixture or again just in the test class constructor:

    var builder = new ConfigurationBuilder()
        .AddJsonFile("testSettings.json", true, true)
        .AddEnvironmentVariables();
    
    var configurationRoot = builder.Build();
    configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
    
    0 讨论(0)
  • 2020-12-12 18:22

    If you intent to use the Mocking Framework as indicated by @TSeng in the comment, you need to add the following dependency in your project.json file.

       "Moq": "4.6.38-alpha",
    

    Once the dependency is restored, using the MOQ framework is as simple as creating an instance of the SampleOptions class and then as mentioned assign it to the Value.

    Here is a code outline how it would look.

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

    Once the mock is setup, you can now pass the mock object to the contructor as

    SampleRepo sr = new SampleRepo(mock.Object);   
    

    HTH.

    FYI I have a git repository that outlines these 2 approaches on Github/patvin80

    0 讨论(0)
提交回复
热议问题