為單元測試填充 IConfiguration
.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 中,您可以通過添加/刪除
Service1和Service2部分來啟動/停用服務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