为单元测试填充 IConfiguration [英] Populate IConfiguration for unit tests

查看:55
本文介绍了为单元测试填充 IConfiguration的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

.NET Core 配置允许有很多选项来添加值(环境变量、json 文件、命令行参数).

.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.

我正在为配置的扩展方法编写单元测试,我认为通过代码在单元测试中填充它比为每个测试加载专用的 json 文件更容易.

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.

我当前的代码:

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

}

更新:

一种特殊情况是空部分".在 json 中看起来像这样.

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


更新 2:


Update 2:

正如 Matthew 在评论中指出的,在 json 中有一个空部分与根本没有该部分的结果相同.我提炼了一个例子,是的,就是这样.期待不同的行为是错误的.

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:

我正在为 IConfiguration 的 2 个扩展方法编写单元测试(实际上是因为 Get...Settings 方法中的值绑定由于某种原因不起作用(但那是一个不同的主题).它们看起来像这样:

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

我的误解是,如果我在 appsettings 中放置一个空白部分,IsService1Configured() 方法将返回 true(现在显然是错误的).我预期的不同之处在于现在 GetService1Settings() 方法返回 null 而不是我预期的具有所有默认值的 MyService1Settings.

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.

我有一个应用程序,其中编译了一个服务/一些服务.根据部署,我需要完全激活/停用服务.这是因为某些(本地或测试设置)无法完全访问完整的基础设施(帮助服务,如缓存、指标......).我通过 appsettings 做到这一点.如果服务已配置(配置部分存在),它将被添加.如果配置部分不存在,它将不会被使用.

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.

  • 在 Visual Studio 中从模板创建一个名为 WebApplication1 的新 API(不带 HTTPS 和身份验证)
  • 删除 Startup 类和 appsettings.Development.json
  • 用下面的代码替换 Program.cs 中的代码
  • 现在在 appsettings.json 中,您可以通过添加/删除 Service1Service2 部分来激活/停用服务
  • 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)));
    }
  }

}

推荐答案

您可以使用 MemoryConfigurationBuilderExtensions 通过字典来提供它.

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

等效的 JSON 为:

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

等效的命令行参数为:

dotnet <myapp.dll> -- --Key1=Value1 --Nested:Key1=NestedValue1 --Nested:Key2=NestedValue2

这篇关于为单元测试填充 IConfiguration的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆