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);
}
}
}
IOptions<T>
you only have to mock Value
to return the class you desire
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.
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
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);
}
}
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; }
}
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");
}
}
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);
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"});
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.
Agree with Aleha that using a testSettings.json configuration file is probably better.
And then, instead of injecting the IOption
var builder = new ConfigurationBuilder()
.AddJsonFile("testSettings.json", true, true)
.AddEnvironmentVariables();
var configurationRoot = builder.Build();
configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
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);
Success story sharing
new OptionsWrapper<SampleOptions>(new SampleOptions());
everywhere