为单元测试填充 IConfiguration
Populate IConfiguration for unit tests
.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()
方法将 return true
(现在显然是错误的)。我预期的不同之处在于现在 GetService1Settings()
方法 returns null
有一个空部分,而不是像我预期的那样具有所有默认值的 MyService1Settings
。
幸运的是,这对我仍然有效,因为我不会有空白部分(或者现在知道我必须避免这些情况)。这只是我在编写单元测试时遇到的一个理论案例。
再往前走(对于那些感兴趣的人)。
我用它做什么?基于配置的服务 activation/deactivation.
我有一个应用程序,其中编译了一个/一些服务。根据部署,我需要完全 activate/deactivate 服务。这是因为某些(本地或测试设置)无法完全访问完整的基础设施(辅助服务,如缓存、指标...)。我通过应用程序设置来做到这一点。如果服务已配置(配置部分存在),它将被添加。如果配置部分不存在,它将不会被使用。
提取示例的完整代码如下。
- 在 Visual Studio 中从模板创建一个名为 WebApplication1 的新 API(没有 HTTPS 和身份验证)
- 删除启动 class 和 appsettings.Development.json
- 用下面的代码替换Program.cs中的代码
- 现在 appsettings.json 您可以 activate/deactivate adding/removing
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)));
}
}
}
AddInMemoryCollection
扩展方法有帮助吗?
您可以将键值集合传递给它:
IEnumerable<KeyValuePair<String,String>>
使用测试可能需要的数据。
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "key", "value" }
});
您可以使用 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
我寻求的解决方案(至少回答了问题标题!)是在解决方案 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;
}
}
您可以使用以下技术模拟 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);
我不想让我的应用程序 class 依赖于 IConfiguration。相反,我创建了一个配置 class 来保存配置,并使用可以从 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 config = new ConfigurationBuilder()
.AddJsonFile("appsettings.Development.json")
.Build();
var services = new ServiceCollection()
.RegisterUseCases()
.AddSingleton<IConfiguration>(config)
.BuildServiceProvider();
.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()
方法将 return true
(现在显然是错误的)。我预期的不同之处在于现在 GetService1Settings()
方法 returns null
有一个空部分,而不是像我预期的那样具有所有默认值的 MyService1Settings
。
幸运的是,这对我仍然有效,因为我不会有空白部分(或者现在知道我必须避免这些情况)。这只是我在编写单元测试时遇到的一个理论案例。
再往前走(对于那些感兴趣的人)。
我用它做什么?基于配置的服务 activation/deactivation.
我有一个应用程序,其中编译了一个/一些服务。根据部署,我需要完全 activate/deactivate 服务。这是因为某些(本地或测试设置)无法完全访问完整的基础设施(辅助服务,如缓存、指标...)。我通过应用程序设置来做到这一点。如果服务已配置(配置部分存在),它将被添加。如果配置部分不存在,它将不会被使用。
提取示例的完整代码如下。
- 在 Visual Studio 中从模板创建一个名为 WebApplication1 的新 API(没有 HTTPS 和身份验证)
- 删除启动 class 和 appsettings.Development.json
- 用下面的代码替换Program.cs中的代码
- 现在 appsettings.json 您可以 activate/deactivate adding/removing
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)));
}
}
}
AddInMemoryCollection
扩展方法有帮助吗?
您可以将键值集合传递给它:
IEnumerable<KeyValuePair<String,String>>
使用测试可能需要的数据。
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "key", "value" }
});
您可以使用 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
我寻求的解决方案(至少回答了问题标题!)是在解决方案 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;
}
}
您可以使用以下技术模拟 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);
我不想让我的应用程序 class 依赖于 IConfiguration。相反,我创建了一个配置 class 来保存配置,并使用可以从 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 config = new ConfigurationBuilder()
.AddJsonFile("appsettings.Development.json")
.Build();
var services = new ServiceCollection()
.RegisterUseCases()
.AddSingleton<IConfiguration>(config)
.BuildServiceProvider();