ChatGPT解决这个技术问题 Extra ChatGPT

.NET Core Unit Testing - Mock IOptions<T>

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 test that class, I want to mock various versions of the options to validate the functionality of the class. Does anyone know how to correctly mock/instantiate/populate IOptions<T> outside of the Startup class?

Here are some samples of the classes I'm working with:

Settings/Options 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; }
    }
}

Class to be tested which uses the Settings:

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

Unit test in a different assembly from the other classes:

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);
        }
    }
}
Are you confusing the meaning of mocking? You mock on an interface and configure it to return a specified value. For IOptions<T>you only have to mock Value to return the class you desire

N
Necoras

You need to manually create and populate an IOptions<SampleOptions> object. You can do so via the Microsoft.Extensions.Options.Options helper class. For example:

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

You can simplify that a bit to:

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

Obviously this isn't very useful as is. You'll need to actually create and populate a SampleOptions object and pass that into the Create method.


I appreciate all the additional answers that show how to use Moq, etc., but this answer is so simple that it's definitely the one I am using. And it works great!
Great answer. Far simpler that relying on a mocking framework.
thanks. I was so tired of writing new OptionsWrapper<SampleOptions>(new SampleOptions()); everywhere
p
patvin80

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


This should be the accepted answer, it works perfectly.
Really wish this worked for me, but it doesn't :( Moq 4.13.1
@alessandrocb so does the accepted answer.
a
aleha

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

This worked nicely for me, cheers! Didn't want to use Moq for something that seemed so simple and didn't want to try populate my own options with configuration settings.
Works great, but the vital missing piece of information is that you need to include the Microsoft.Extensions.Configuration.Binder nuget package, otherwise you don't get the "Get" extension method available.
I had to run dotnet add package Microsoft.Extensions.Configuration.Json to get this to work. Great answer!
I had also to change the properties of the appsettings.json file to use the file in the bin file, as Directory.GetCurrentDirectory() was returning the content of the bin file. In "Copy to output directory" of appsettings.json, I set the value to "Copy if newer".
m
matei

You can always create your options via Options.Create() and than simply use AutoMocker.Use(options) before actually creating the mocked instance of the repository you're testing. Using AutoMocker.CreateInstance<>() makes it easier to create instances without manually passing parameters

I've changed you're SampleRepo a bit in order to be able to reproduce the behavior I think you want to achieve.

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

F
Frank

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

You aren't testing anything useful. The framework for DI my Microsoft is already unit tested. As it stands this is really an integration test (integration with a 3rd party framework).
@ErikPhilips My code shows how to mock IOptions as requested by the OP. I agree that it does not test anything useful in itself, but it can be useful testing something else.
R
Robert Corvus

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

S
Sujoy

Using the Microsoft.Extensions.Options.Options class:

var someOptions= Options.Create(new SampleOptions(){Field1="Value1",Field2="Value2"});

OR

var someOptions= Options.Create(new SampleOptions{Field1="Value1",Field2="Value2"});

M
Mithrandir

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.


P
Pang

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

And then, instead of injecting the IOption, 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);

S
Shadi Bahrami

first add "appsettings.json" file in the root unitTestProject then use this code: private readonly Mock _fileRepMock; private IOptions _options; public FileServiceTest() { _fileRepMock = new Mock(); var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .Build(); _options = Options.Create(config.GetSection("File").Get()); } now you can use _options in the mock repository FileService fileService = new FileService(_fileRepMock.Object, _options);