【问题标题】:Populate IConfiguration for unit tests为单元测试填充 IConfiguration
【发布时间】:2019-08-25 03:19:48
【问题描述】:

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

我只是不知道如何通过代码填充它并找到答案。

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

我当前的代码:

[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 中看起来像这样。

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

更新 2:

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

那么我该怎么做以及我期望什么:

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

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 具有所有默认值。

幸运的是,这仍然对我有用,因为我不会有空白部分(或者现在知道我必须避免这些情况)。这只是我在编写单元测试时遇到的一个理论案例。

更进一步(对于那些有兴趣的人)。

我用它做什么?基于配置的服务激活/停用。

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


提炼示例的完整代码如下。

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

}

【问题讨论】:

    标签: asp.net-core .net-core xunit asp.net-core-2.2 .net-core-2.2


    【解决方案1】:

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

    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 是:

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

    等效的环境变量将是(假设没有前缀/不区分大小写):

    Key1=Value1
    Nested__Key1=NestedValue1
    Nested__Key2=NestedValue2
    

    等效的命令行参数是:

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

    【讨论】:

    • 是的,可以。我更新了我的问题以反映缺失的部分。
    • 您应该更新您的问题以包括您期望发生的事情。拥有一个空的 JSON 节点会产生与根本没有该节点相同的输出。
    • 确实你是对的。一个空白部分似乎已被删除并且不存在。我在我的问题中添加了更新 2,并附上了我(错误地)预期会发生什么以及为什么发生的完整示例。
    • 嗯,单元测试显示配置绑定失败,因为我只是为属性定义了get而不是set。
    • @Matthew 将 json 添加到您的示例中会有所帮助
    【解决方案2】:

    AddInMemoryCollection 扩展方法有帮助吗?

    您可以将键值集合传递给它: IEnumerable&lt;KeyValuePair&lt;String,String&gt;&gt; 包含您可能需要进行测试的数据。

    var builder = new ConfigurationBuilder();
    
    builder.AddInMemoryCollection(new Dictionary<string, string>
    {
         { "key", "value" }
    });
    

    【讨论】:

    • 是的,可以。我更新了我的问题以反映缺失的部分。
    【解决方案3】:

    我不希望我的应用程序类依赖于 IConfiguration。相反,我创建了一个配置类来保存配置,并使用一个可以从 IConfiguration 对其进行初始化的构造函数,如下所示:

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

    然后在您的ConfigureServices 中,您只需要这样做:

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

    用于测试:

    var config = new WidgetProcessorConfig
    {
        QueueLength = 18
    };
    var widgetProcessor = new WidgetProcessor(config);
    

    【讨论】:

    • 当然,应用程序类依赖于 IConfiguration 是一种不好的做法。但是一旦配置变得复杂,仅仅绑定是不够的。我为每个配置类实现了一个验证系统,以在启动时引发错误。并且该代码需要进行单元测试。 :-)
    【解决方案4】:

    我寻求的解决方案(至少回答了问题标题!)是在解决方案 testsettings.json 中使用设置文件并将其设置为“始终复制”。

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

    【讨论】:

    • 大家好,AddJsonFile 似乎在 .net 5.0 端源上做了一些修改:https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions。添加json文件?视图 = dotnet-plat-ext-5.0 # Microsoft_Extensions_Configuration_JsonConfigurationExtensions_AddJsonFile_Microonsoft
    • 我正在尝试在 VS 2022 中将其与 .net 6.0 一起使用,但出现错误,ConfigurationBuilder does not contain AddJsonFile
    【解决方案5】:

    您可以使用以下技术来模拟IConfiguration.GetValue&lt;T&gt;(key) 扩展方法。

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

    【讨论】:

    • 这个答案更适合这个(封闭的)问题:stackoverflow.com/questions/43618686/… 但我找不到与这个主题相关的任何其他东西与一个有效的起订量方法。很多人一直在问这个问题,但您能找到的唯一答案与ConfigurationBuilder 有关。如果上面的问题是开放的,我会在那里发布。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-25
    • 2018-09-05
    • 1970-01-01
    • 2023-02-22
    • 1970-01-01
    相关资源
    最近更新 更多