Populate IConfiguration for unit tests
Asked Answered
G

7

111

.NET Core configuration allows so many options to add values (environment variables, json files, command line args).

I just can't figure out and find an answer how to populate it via code.

I am writing unit tests for extension methods to configurations and I thought populating it in the unit tests via code would be easier than loading dedicated json files for each test.

My current code:

[Fact]
public void Test_IsConfigured_Positive()
{

  // test against this configuration
  IConfiguration config = new ConfigurationBuilder()
    // how to populate it via code
    .Build();

  // the extension method to test
  Assert.True(config.IsConfigured());

}

Update:

One special case is the "empty section" which would look like this in json.

{
  "MySection": {
     // the existence of the section activates something triggering IsConfigured to be true but does not overwrite any default value
   }
 }

Update 2:

As Matthew pointed out in the comments having an empty section in the json gives the same result as not having the section at all. I distilled an example and yes, that is the case. I was wrong expecting a different behaviour.

So what do I do and what did I expect:

I am writing unit tests for 2 extension methods for IConfiguration (actually because the binding of values in Get...Settings method does not work for some reason (but thats a different topic). They look like this:

public static bool IsService1Configured(this IConfiguration configuration)
{
  return configuration.GetSection("Service1").Exists();
}

public static MyService1Settings GetService1Settings(this IConfiguration configuration)
{
  if (!configuration.IsService1Configured()) return null;

  MyService1Settings settings = new MyService1Settings();
  configuration.Bind("Service1", settings);

  return settings;
}

My missunderstanding was that if I place an empty section in the appsettings the IsService1Configured() method would return true (which is obviously wrong now). The difference I expected is with having an empty section now the GetService1Settings() method returns null and not as I expected the MyService1Settings with all default values.

Fortunately this still works for me since I won't have empty sections (or now know that I have to avoid those cases). It was just one theoretical case I came across while writing the unit tests.

Further down the road (for those interested in).

For what do I use it? Configuration based service activation/deactivation.

I have an application which has a service / some services compiled into it. Depending on the deployment I need to activate/deactivate the services completly. This is because some (local or testings setups) do not have full access to a complete infrastructure (helper services like caching, metrics...). And I do that via the appsettings. If the service is configured (the config section exists) it will be added. If the config section is not present it will not be used.


The full code for the distilled example is below.

  • in Visual Studio create a new API named WebApplication1 from the templates (without HTTPS and Authentication)
  • delete the Startup class and appsettings.Development.json
  • replace the code in Program.cs with the code below
  • now in appsettings.json you can activate/deactivate the services by adding/removing Service1 and Service2 section
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;

namespace WebApplication1
{

  public class MyService1Settings
  {
  public int? Value1 { get; set; }
  public int Value2 { get; set; }
  public int Value3 { get; set; } = -1;
  }

  public static class Service1Extensions
  {

  public static bool IsService1Configured(this IConfiguration configuration)
  {
  return configuration.GetSection("Service1").Exists();
  }

  public static MyService1Settings GetService1Settings(this IConfiguration configuration)
  {
  if (!configuration.IsService1Configured()) return null;

  MyService1Settings settings = new MyService1Settings();
  configuration.Bind("Service1", settings);

  return settings;
  }

  public static IServiceCollection AddService1(this IServiceCollection services, IConfiguration configuration, ILogger logger)
  {

  MyService1Settings settings = configuration.GetService1Settings();

  if (settings == null) throw new Exception("loaded MyService1Settings are null (did you forget to check IsConfigured in Startup.ConfigureServices?) ");

  logger.LogAsJson(settings, "MyServiceSettings1: ");

  // do what ever needs to be done

  return services;
  }

  public static IApplicationBuilder UseService1(this IApplicationBuilder app, IConfiguration configuration, ILogger logger)
  {

  // do what ever needs to be done

  return app;
  }

  }

  public class Program
  {

    public static void Main(string[] args)
    {
      CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
      .ConfigureLogging
        (
        builder => 
          {
            builder.AddDebug();
            builder.AddConsole();
          }
        )
      .UseStartup<Startup>();
      }

    public class Startup
    {

      public IConfiguration Configuration { get; }
      public ILogger<Startup> Logger { get; }

      public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
      {
      Configuration = configuration;
      Logger = loggerFactory.CreateLogger<Startup>();
      }

      // This method gets called by the runtime. Use this method to add services to the container.
      public void ConfigureServices(IServiceCollection services)
      {

      // flavour 1: needs check(s) in Startup method(s) or will raise an exception
      if (Configuration.IsService1Configured()) {
      Logger.LogInformation("service 1 is activated and added");
      services.AddService1(Configuration, Logger);
      } else 
      Logger.LogInformation("service 1 is deactivated and not added");

      // flavour 2: checks are done in the extension methods and no Startup cluttering
      services.AddOptionalService2(Configuration, Logger);

      services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {

      if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

      // flavour 1: needs check(s) in Startup method(s) or will raise an exception
      if (Configuration.IsService1Configured()) {
        Logger.LogInformation("service 1 is activated and used");
        app.UseService1(Configuration, Logger); }
      else
        Logger.LogInformation("service 1 is deactivated and not used");

      // flavour 2: checks are done in the extension methods and no Startup cluttering
      app.UseOptionalService2(Configuration, Logger);

      app.UseMvc();
    }
  }

  public class MyService2Settings
  {
    public int? Value1 { get; set; }
    public int Value2 { get; set; }
    public int Value3 { get; set; } = -1;
  }

  public static class Service2Extensions
  {

  public static bool IsService2Configured(this IConfiguration configuration)
  {
    return configuration.GetSection("Service2").Exists();
  }

  public static MyService2Settings GetService2Settings(this IConfiguration configuration)
  {
    if (!configuration.IsService2Configured()) return null;

    MyService2Settings settings = new MyService2Settings();
    configuration.Bind("Service2", settings);

    return settings;
  }

  public static IServiceCollection AddOptionalService2(this IServiceCollection services, IConfiguration configuration, ILogger logger)
  {

    if (!configuration.IsService2Configured())
    {
      logger.LogInformation("service 2 is deactivated and not added");
      return services;
    }

    logger.LogInformation("service 2 is activated and added");

    MyService2Settings settings = configuration.GetService2Settings();
    if (settings == null) throw new Exception("some settings loading bug occured");

    logger.LogAsJson(settings, "MyService2Settings: ");
    // do what ever needs to be done
    return services;
  }

  public static IApplicationBuilder UseOptionalService2(this IApplicationBuilder app, IConfiguration configuration, ILogger logger)
  {

    if (!configuration.IsService2Configured())
    {
      logger.LogInformation("service 2 is deactivated and not used");
      return app;
    }

    logger.LogInformation("service 2 is activated and used");
    // do what ever needs to be done
    return app;
  }
}

  public static class LoggerExtensions
  {
    public static void LogAsJson(this ILogger logger, object obj, string prefix = null)
    {
      logger.LogInformation(prefix ?? string.Empty) + ((obj == null) ? "null" : JsonConvert.SerializeObject(obj, Formatting.Indented)));
    }
  }

}
Gilleod answered 3/4, 2019 at 14:37 Comment(0)
S
233

You can use MemoryConfigurationBuilderExtensions to provide it via a dictionary.

using Microsoft.Extensions.Configuration;

var myConfiguration = new Dictionary<string, string>
{
    {"Key1", "Value1"},
    {"Nested:Key1", "NestedValue1"},
    {"Nested:Key2", "NestedValue2"}
};

var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(myConfiguration)
    .Build();

The equivalent JSON would be:

{
  "Key1": "Value1",
  "Nested": {
    "Key1": "NestedValue1",
    "Key2": "NestedValue2"
  }
}

The equivalent Environment Variables would be (assuming no prefix / case insensitive):

Key1=Value1
Nested__Key1=NestedValue1
Nested__Key2=NestedValue2

The equivalent Command Line parameters would be:

dotnet myapp.dll \
  -- \
  --Key1=Value1 \
  --Nested:Key1=NestedValue1 \
  --Nested:Key2=NestedValue2
Supererogate answered 3/4, 2019 at 14:43 Comment(5)
Yes, that would work. I updated my question to reflect the missing piece.Gilleod
You should update your question to include what you expect to happen. Having an empty JSON node results in the same output as not even having that node there at all.Supererogate
indeed you were right. An empty section seems to be removed and does not exist. I added Update 2 to my question with a full example what I (wrongly) expected to happen and why.Gilleod
well, the unit test revealed that configuration binding failed because I just defined the get for the property but not the set.Gilleod
With nullable enabled and warnings as errors this needs to be var myConfiguration = new Dictionary<string, string?>Erichericha
S
28

The solution I went for (which answers the question title at least!) is to use a settings file in the solution testsettings.json and set it to "Copy Always".

private IConfiguration _config;

public UnitTestManager()
{
    IServiceCollection services = new ServiceCollection();

    services.AddSingleton<IConfiguration>(Configuration);
}

public IConfiguration Configuration
{
    get
    {
        if (_config == null)
        {
            var builder = new ConfigurationBuilder().AddJsonFile($"testsettings.json", optional: false);
            _config = builder.Build();
        }

        return _config;
    }
}
Strap answered 13/2, 2020 at 15:42 Comment(3)
Hi guys, AddJsonFile seems a bit modified on the .net 5.0 side source: https: //learn.microsoft.com/tr-tr/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile? view = dotnet-plat-ext-5.0 # Microsoft_Extensions_Configuration_JsonConfigurationExtensions_AddJsonFile_MicroonsoftStites
I'm trying to use this with .net 6.0 in VS 2022, but I get an error, ConfigurationBuilder does not contain AddJsonFile Nicky
You need to add the following namespace : Microsoft.Extensions.Configuration.JsonTompion
C
5

Would AddInMemoryCollection extension method help?

You can pass a key-value collection into it: IEnumerable<KeyValuePair<String,String>> with the data you might need for a test.

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(new Dictionary<string, string>
{
     { "key", "value" }
});
Carberry answered 3/4, 2019 at 14:42 Comment(0)
N
5

You can use the following technique to mock IConfiguration.GetValue<T>(key) extension method.

var configuration = new Mock<IConfiguration>();
var configSection = new Mock<IConfigurationSection>();

configSection.Setup(x => x.Value).Returns("fake value");
configuration.Setup(x => x.GetSection("MySection")).Returns(configSection.Object);
//OR
configuration.Setup(x => x.GetSection("MySection:Value")).Returns(configSection.Object);
Neritic answered 19/3, 2020 at 17:29 Comment(1)
This answer is more for this (closed) question: #43619186 But I could not find anything else related to this topic with a working Moq approach. A lot of people been asking about it but the only answers you can find are related to ConfigurationBuilder. If the above question was open, I would post it there.Neritic
K
3

I prefer not to have my application classes dependent on IConfiguration. Instead, I create a configuration class to hold the config, with a constructor that can initialise it from IConfiguration, like this:

public class WidgetProcessorConfig
{
    public int QueueLength { get; set; }
    public WidgetProcessorConfig(IConfiguration configuration)
    {
        configuration.Bind("WidgetProcessor", this);
    }
    public WidgetProcessorConfig() { }
}

then in your ConfigureServices, you just have to do:

services.AddSingleton<WidgetProcessorConfig>();
services.AddSingleton<WidgetProcessor>();

and for testing:

var config = new WidgetProcessorConfig
{
    QueueLength = 18
};
var widgetProcessor = new WidgetProcessor(config);
Killdeer answered 27/4, 2020 at 12:11 Comment(3)
for sure it is a bad practice that application classes depend on IConfiguration. But once the configuration becomes complex simply bind is not enough. I implemented a validation system for each config classes to raise errors on startup. And that code needed to be unit tested. :-)Gilleod
Would that refresh settings as config source (e.g. json file) changes? Since it's a singleton I guess not. I suppose making it scoped would fix that, but that wouldn't be great performance-wise. I guess that's why these IOptionsMonitor and IOptionsSnapshot interfaces exist, but they're a pain to mock in unit tests. So you have to choose between ease of use during unit tests, or better performance...Filter
@VincentSels AFAIK the IConfiguration is only loaded from the file once when the application starts, so making it scoped wouldn't help. However, if it's a web app, I believe the web server will automatically reload the app when the appsettings.json file changes on disk, so it should work.Killdeer
W
2

I would do it like this:

IConfiguration config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.Development.json")    
    .Build();

using var services = new ServiceCollection()
                .AddSingleton<IConfiguration>(config)
               // -> add your DI needs here
                .BuildServiceProvider();

or when you have some custom dependency that you need to inject using your own extension method, say RegisterUseCases()

IConfiguration config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.Development.json")    
    .Build();

using var services = new ServiceCollection()
                .RegisterUseCases()//-> Testing the use case that uses the IConfiguration
                .AddSingleton<IConfiguration>(config)
                .BuildServiceProvider();


var systemUnderTest= services.GetRequiredService<IMyConfigClass>();
... 

You can now test classes that depend on IConfiguration

Wisent answered 5/5, 2022 at 15:13 Comment(3)
what is IMyConfigClass ? I need use appsettings.json (section - IOptions) in TestMethod (MSTest project)Sender
@Kiquenet, the interface is just a sample for DI that uses IConfigurationWisent
@Kiquenet, you can use appsettings.json, or load any json using .AddJsonFile(path) extension methodWisent
C
0

Add Array sample for InMemory json configuration:

using Microsoft.Extensions.Configuration;

var myConfiguration = new Dictionary<string, string>
{
   {"ArrayKeySample:0", "valueA"},
   {"ArrayKeySample:1", "valueB"},
   {"ArrayKeySample:2", "valueC"}
};

var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(myConfiguration)
    .Build();
Chamaeleon answered 7/7, 2022 at 7:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.