.NET 7单元测试中从appsettings加载的数组丢失空字符串问题
解决.NET 7中TestServer加载配置时空字符串数组元素丢失的问题
.NET 7项目里遇到这么个情况:
- appsettings里的二维字符串数组包含空字符串,API里用
config.GetSection("AppSettings").GetSection("Mapping").Get<string[][]>()能正常拿到每个子数组的3个元素 - 但单元测试用TestServer加载同一份配置时,空字符串直接被删了,导致断言失败;可直接用ConfigurationBuilder构建的ConfigurationRoot却能正常读取完整数组
- 切回.NET 6就没问题,确定是.NET 7的配置逻辑变了,而且降级TestHost之类的NuGet包也没用
- 要求不能改API的业务代码,只能调单元测试代码解决
方案一:自定义JSON配置提供器(推荐)
TestServer在.NET 7里的配置加载链和直接构建ConfigurationRoot不一样,我们可以自己写个配置提供器,强制保留空字符串元素。
- 编写自定义配置源和提供器(放在测试项目中):
public class PreserveEmptyStringJsonConfigurationSource : JsonConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new PreserveEmptyStringJsonConfigurationProvider(this); } } public class PreserveEmptyStringJsonConfigurationProvider : JsonConfigurationProvider { public PreserveEmptyStringJsonConfigurationProvider(PreserveEmptyStringJsonConfigurationSource source) : base(source) { } protected override void Load(Stream stream) { var jsonDoc = JsonDocument.Parse(stream, new JsonDocumentOptions { AllowTrailingCommas = true }); if (jsonDoc.RootElement.ValueKind != JsonValueKind.Object) { throw new FormatException($"配置文件 '{Source.Path}' 不是有效的JSON对象"); } var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); TraverseElement(jsonDoc.RootElement, null, data); Data = data; } private void TraverseElement(JsonElement element, string? path, Dictionary<string, string> data) { switch (element.ValueKind) { case JsonValueKind.Object: foreach (var prop in element.EnumerateObject()) { var newPath = string.IsNullOrEmpty(path) ? prop.Name : $"{path}:{prop.Name}"; TraverseElement(prop.Value, newPath, data); } break; case JsonValueKind.Array: for (int i = 0; i < element.GetArrayLength(); i++) { var newPath = string.IsNullOrEmpty(path) ? $"{i}" : $"{path}:{i}"; TraverseElement(element[i], newPath, data); } break; case JsonValueKind.String: // 不管是不是空字符串,直接存入配置字典 data[path!] = element.GetString()!; break; case JsonValueKind.Number: case JsonValueKind.True: case JsonValueKind.False: data[path!] = element.ToString(); break; case JsonValueKind.Null: data[path!] = null!; break; default: throw new FormatException($"不支持的JSON类型 '{element.ValueKind}',路径:{path}"); } } }
- 单元测试中使用自定义配置源构建TestServer:
[Fact] public async Task Test_Mapping_Configuration() { // 用自定义配置源加载appsettings,确保空字符串不丢失 var configBuilder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .Add(new PreserveEmptyStringJsonConfigurationSource { Path = "appsettings.json", Optional = false, ReloadOnChange = false }) .AddEnvironmentVariables(); var config = configBuilder.Build(); // 给TestServer传入这个自定义配置启动 var webHost = new WebHostBuilder() .UseConfiguration(config) .UseStartup<Startup>(); // 替换为你的项目Startup类 using var server = new TestServer(webHost); var client = server.CreateClient(); // 执行测试请求 var response = await client.GetAsync("/api/your-endpoint"); response.EnsureSuccessStatusCode(); // 此处执行断言逻辑,配置中的空字符串已被正常保留 }
方案二:直接替换TestServer的配置实例
如果不想编写自定义提供器,也可以先构建一个能正确读取空字符串的配置,再替换TestServer服务容器中的IConfiguration实例:
[Fact] public async Task Test_Mapping_Configuration() { // 先构建一个能正确读取空字符串的配置实例 var correctConfig = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); // 启动TestServer var webHost = new WebHostBuilder() .UseStartup<Startup>(); using var server = new TestServer(webHost); // 替换服务容器中的IConfiguration为正确实例 var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton<IConfiguration>(correctConfig); var serviceProvider = serviceCollection.BuildServiceProvider(); // 直接用正确配置做断言验证 var mapping = correctConfig.GetSection("AppSettings").GetSection("Mapping").Get<string[][]>(); Assert.Equal(3, mapping[0].Length); // 断言现在可以正常通过 // 继续执行API测试逻辑 var client = server.CreateClient(); // ... }
问题原因说明
.NET 7中,TestServer依赖的WebHost默认配置加载逻辑会过滤JSON数组中的空字符串元素;但直接使用ConfigurationBuilder时,用的是原始的JsonConfigurationProvider,不会做过滤处理。通过自定义配置提供器对齐逻辑,或者直接替换TestServer的配置实例,就能让单元测试的配置行为和API运行时一致,无需修改API业务代码。
内容的提问来源于stack exchange,提问作者majo
相关产品推荐
相关产品推荐

