Asp.net-Core

為單元測試填充 IConfiguration

  • April 3, 2019

.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 在評論中指出的那樣,在 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)));
   }
 }

}

您可以使用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

引用自:https://stackoverflow.com/questions/55497800