如何避免使用Moq时LoggerFactory.Create抛出异常?
更新相关包(大概率是Microsoft.Extensions.DependencyInjection)后,测试代码在以下行抛出异常:
var userGetLogger = LoggerFactory.Create(b => b.AddConfiguration(Configuration)).CreateLogger<UserGet>();
其中Configuration是通过new Mock<IConfiguration>().Object创建的Moq实例。
场景背景:在Azure Function UserPost中需要调用同项目的另一个Azure Function UserGet,必须符合UserGet的DI构造函数签名(需要传入ILogger<UserGet>类型的日志实例)。生产环境代码运行正常,但测试失败。
当前临时方案:给UserPost构造函数传入null,做空值判断后调用LoggerFactory.Create(b => {})(不会抛异常),但这种为了测试而修改业务代码的方式很别扭。
需求:
- 能否配置Moq避免该异常?
- 如果不行,能否在抛出异常前提前检测问题?
异常堆栈跟踪
at Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue[T](IConfiguration configuration, String key, T defaultValue) at Microsoft.Extensions.Logging.LoggerFilterConfigureOptions.LoadDefaultConfigValues(LoggerFilterOptions options) at Microsoft.Extensions.Logging.LoggerFilterConfigureOptions.Configure(LoggerFilterOptions options) at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name) at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0() at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) at System.Lazy`1.CreateValue() at System.Lazy`1.get_Value() at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions) at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name) at Microsoft.Extensions.Options.OptionsMonitor`1.get_CurrentValue() at Microsoft.Extensions.Logging.LoggerFactory..ctor(IEnumerable`1 providers, IOptionsMonitor`1 filterOption, IOptions`1 options) at System.RuntimeMethodHandle.InvokeMethod(Object target, Span`1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(Type serviceType) at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider) at Microsoft.Extensions.Logging.LoggerFactory.Create(Action`1 configure) at UserAccess.Functions.UserPost.<Run>d__10.MoveNext() in H:\Repos\EasySpect\Portal\UserAccess\Project\Functions\UserPost.cs:line 43
UserGet类代码片段
public class UserGet { IUsers Api { get; } ILogger Logger { get; } public UserGet(IUsers api, ILogger<UserGet> logger) { Api = api; Logger = logger; } [FunctionName(nameof(UserGet))] public async Task<Guid> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = "user/{email}")] HttpRequest req, string email) { if (email == null) { Logger.LogWarning("User email is not present in the URL"); return Guid.Empty; } return await GetUser(email); } }
一、配置Moq模拟IConfiguration避免异常
异常根源是LoggerFilterConfigureOptions尝试从配置中读取日志过滤相关的默认值(比如Logging:LogLevel节点),而空的MoqIConfiguration无法正确响应这些读取操作。可以给Moq配置必要的节点模拟:
var configMock = new Mock<IConfiguration>(); // 模拟日志配置节点,设置默认日志级别 configMock.Setup(c => c.GetSection("Logging:LogLevel:Default")).Returns(Mock.Of<IConfigurationSection>(s => s.Value == "Information")); configMock.Setup(c => c.GetSection("Logging:LogLevel:Microsoft")).Returns(Mock.Of<IConfigurationSection>(s => s.Value == "Warning")); // 如果还需要其他节点,按需添加,比如特定类的日志级别 configMock.Setup(c => c.GetSection("Logging:LogLevel:UserAccess.Functions.UserGet")).Returns(Mock.Of<IConfigurationSection>(s => s.Value == "Information")); var Configuration = configMock.Object;
这样当LoggerFactory尝试读取配置时,Moq能返回预期的值,避免ConfigurationBinder.GetValue抛出异常。
二、直接模拟ILogger替代手动创建
既然是测试场景,完全可以直接MockILogger<UserGet>,无需手动通过LoggerFactory创建,更简洁且避免配置依赖:
// 在测试中直接创建Mock的ILogger<UserGet> var loggerMock = new Mock<ILogger<UserGet>>(); // 如果需要验证日志调用,可以添加Setup,否则空Mock即可 // 实例化UserGet时传入该Mock var userGet = new UserGet(apiMock.Object, loggerMock.Object);
这种方式彻底绕开了LoggerFactory和IConfiguration的依赖问题,测试代码更聚焦业务逻辑,也不用修改生产代码。
三、提前检测配置有效性(备选方案)
如果一定要保留手动创建LoggerFactory的逻辑,可以在调用前检测Configuration是否包含必要节点:
bool IsLoggingConfigValid(IConfiguration config) { return config.GetSection("Logging:LogLevel").Exists() && !string.IsNullOrEmpty(config.GetSection("Logging:LogLevel:Default").Value); } // 使用时 var loggerFactory = IsLoggingConfigValid(Configuration) ? LoggerFactory.Create(b => b.AddConfiguration(Configuration)) : LoggerFactory.Create(b => {}); var userGetLogger = loggerFactory.CreateLogger<UserGet>();
这段代码可以放在生产代码中,既不影响正常环境(配置有效时走正常逻辑),测试环境中配置无效时自动降级到无配置的LoggerFactory,避免异常,同时不用做null判断,代码更干净。
内容的提问来源于stack exchange,提问作者noontz

