填充IConfiguration以进行单元测试

mon*_*nty 4 xunit .net-core asp.net-core asp.net-core-2.2 .net-core-2.2

.NET Core配置允许许多选项来添加值(环境变量,json文件,命令行args)。

我只是想不通,找到一个如何通过代码填充它的答案。

我正在编写用于配置扩展方法的单元测试,我认为通过代码在单元测试中填充它比为每个测试加载专用的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());

  }
Run Code Online (Sandbox Code Playgroud)

更新:

一种特殊情况是“空部分”,它在json中看起来像这样。

  {
    "MySection": {
       // the existence of the section activates something triggering IsConfigured to be true but does not overwrite any default value
     }
   }
Run Code Online (Sandbox Code Playgroud)

更新2:

就像马修(Matthew)在评论中指出的那样,在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;
  }
Run Code Online (Sandbox Code Playgroud)

我的误解是,如果我在appsettings中放置一个空白部分,该IsService1Configured()方法将返回true(现在显然是错误的)。我期望的区别是现在有一个空部分,该GetService1Settings()方法返回,null而不是我期望的MyService1Settings所有默认值。

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

再往前走(对于那些感兴趣的人)。

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

我有一个应用程序,其中已包含服务/一些服务。根据部署情况,我需要完全激活/停用服务。这是因为某些(本地或测试设置)无法完全访问完整的基础结构(诸如缓存,指标等辅助服务)。我通过appsettings来做到这一点。如果配置了服务(config部分存在),则将添加该服务。如果config节不存在,则将不使用它。


摘录示例的完整代码如下。

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

  }
Run Code Online (Sandbox Code Playgroud)

Mat*_*hew 9

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

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

var configuration = new ConfigurationBuilder()
    .AddInMemoryCollection(myConfiguration)
    .Build();
Run Code Online (Sandbox Code Playgroud)


noe*_*cus 7

我寻求的解决方案(至少回答了问题标题!)是在解决方案中使用设置文件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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 您需要添加以下命名空间:Microsoft.Extensions.Configuration.Json (2认同)

Ant*_*kov 5

AddInMemoryCollection扩展方法有帮助吗?

您可以将键值集合传递给其中: IEnumerable<KeyValuePair<String,String>>以及测试可能需要的数据。

var builder = new ConfigurationBuilder();

builder.AddInMemoryCollection(new Dictionary<string, string>
{
     { "key", "value" }
});
Run Code Online (Sandbox Code Playgroud)


Ser*_*erj 5

您可以使用以下技术来模拟IConfiguration.GetValue<T>(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);
Run Code Online (Sandbox Code Playgroud)