自定义 NLog 过滤器的单元测试 class
Unit-testing of custom NLog filter class
在实现自定义 NLog 过滤器 https://github.com/NLog/NLog/wiki/Filtering-log-messages#fully-dynamic-filtering 时,我尝试依赖 WhenMethodFilter
,它接受 lambda 回调 ShouldLogEvent
。然而,为了对回调 lambda 进行单元测试,我需要在生成配置的 class 中将其设为 public,这并不理想——我讨厌仅仅为了测试而制作方法 public。
private void ReconfigureNlog(object state)
{
var nlogConfig = ConstructNlogConfiguration();
foreach (var rule in nlogConfig.LoggingRules)
{
rule.Filters.Add(new WhenMethodFilter(ShouldLogEvent));
}
// TODO: maybe no need to reconfigure every time but just modify filter reference?
NLog.Web.NLogBuilder.ConfigureNLog(nlogConfig);
}
另一种方法是继承 Filter
基础 class 并尝试用单元测试覆盖它。但是它没有 public 接口的问题:
internal FilterResult GetFilterResult(LogEventInfo logEvent)
protected abstract FilterResult Check(LogEventInfo logEvent);
这使得它也不可测试,除非我自己制作方法 public。尽管这似乎是一个小问题,但我很好奇是否有更好的方法。从我的角度来看,将 GetFilterResult
设置为 internal 是绝对没有必要的,尽管它有点遵循最佳设计实践。想法?
更新1.待测class代码:
public class TenancyLogFilter: Filter
{
private readonly ITenancyLoggingConfiguration _loggingConfigurationConfig;
private readonly IHttpContextAccessor _httpContextAccessor;
public TenancyLogFilter(ITenancyLoggingConfiguration loggingConfigurationConfig, IHttpContextAccessor httpContextAccessor)
{
_loggingConfigurationConfig = loggingConfigurationConfig;
_httpContextAccessor = httpContextAccessor;
}
protected override FilterResult Check(LogEventInfo logEvent)
{
var result = FilterResult.Neutral;
if (CanEmitEvent(logEvent.Level))
{
result = FilterResult.Log;
}
return result;
}
private LogLevel GetMinLogLevel()
{
var level = LogLevel.Trace;
if (!_loggingConfigurationConfig.TenantMinLoggingLevel.Any())
return level;
var context = _httpContextAccessor?.HttpContext;
if (context == null)
return level;
if (context.Request.Headers.TryGetValue(CustomHeaders.TenantId, out var tenantIdHeaders))
{
var currentTenant = tenantIdHeaders.First();
if (_loggingConfigurationConfig.TenantMinLoggingLevel.ContainsKey(currentTenant))
{
level = _loggingConfigurationConfig.TenantMinLoggingLevel[currentTenant];
}
}
return level;
}
private bool CanEmitEvent(LogLevel currentLogLevel)
{
return currentLogLevel >= GetMinLogLevel();
}
}
通常丑陋的技巧是创建方法 internal
,然后将其添加到主项目中的 AssemblyInfo.cs:
using System;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleToAttribute("Tenancy.UnitTests")]
然后单元测试项目Tenancy.UnitTests
将被允许对主项目的内部方法进行单元测试。
出于测试 class 的目的,您可以从主题 class 派生以公开调用被测成员所需的内容,因为目标成员受到保护。
无法 change/access 内部成员,但在这种情况下,源显示它是一个简单的实现
/// <summary>
/// Gets the result of evaluating filter against given log event.
/// </summary>
/// <param name="logEvent">The log event.</param>
/// <returns>Filter result.</returns>
internal FilterResult GetFilterResult(LogEventInfo logEvent)
{
return Check(logEvent);
}
从那里开始,只需要提供必要的依赖项即可完成测试。
例如。
[TestClass]
public class TenancyLogFilterTests {
[TestMethod]
public void Should_Log_LogEvent() {
//Arrange
string expectedId = Guid.NewGuid().ToString();
LogLevel expectedLevel = LogLevel.Error;
FilterResult expected = FilterResult.Log;
var context = new DefaultHttpContext();
context.Request.Headers.Add(CustomHeaders.TenantId, expectedId);
var accessor = Mock.Of<IHttpContextAccessor>(_ => _.HttpContext == context);
var level = new Dictionary<string, LogLevel> {
{ expectedId, expectedLevel }
};
var config = Mock.Of<ITenancyLoggingConfiguration>(_ => _.TenantMinLoggingLevel == level);
var subject = new TestTenancyLogFilter(config, accessor);
var info = new LogEventInfo { Level = expectedLevel };
//Act
FilterResult actual = subject.GetFilterResult(info);
//Assert - FluentAssertions
actual.Should().Be(expected);
}
class TestTenancyLogFilter : TenancyLogFilter {
public TestTenancyLogFilter(ITenancyLoggingConfiguration loggingConfigurationConfig, IHttpContextAccessor httpContextAccessor)
: base(loggingConfigurationConfig, httpContextAccessor) { }
public FilterResult GetFilterResult(LogEventInfo logEvent) {
return Check(logEvent);
}
}
}
这样可以隔离测试,也可以解决外部第 3 方依赖项提供的限制。
实际过滤器保持不变,无需暴露任何额外内容。
从您的原始代码示例中,请注意,当遇到这样的阻塞问题时,应将其视为设计问题和提取服务抽象的标志。
例如
public interface ILogEventAssessor {
FilterResult GetFilterResult(LogEventInfo logEvent);
}
其实现将封装在自定义 TenancyLogFilter
过滤器中完成的操作,并注入目标 class
private readonly ILogEventAssessor service;
//...assumed injected service
private void ReconfigureNlog(object state) {
var nlogConfig = ConstructNlogConfiguration();
foreach (var rule in nlogConfig.LoggingRules) {
rule.Filters.Add(new WhenMethodFilter(ShouldLogEvent));
}
// TODO: maybe no need to reconfigure every time but just modify filter reference?
NLog.Web.NLogBuilder.ConfigureNLog(nlogConfig);
}
private FilterResult ShouldLogEvent(LogEventInfo logEvent) {
return service.GetFilterResult(logEvent);
}
//...
现在真的不需要测试第 3 方过滤器来验证您的逻辑了。
您可以测试您的 ILogEventAssessor
实施以单独验证您的自定义逻辑。
在实现自定义 NLog 过滤器 https://github.com/NLog/NLog/wiki/Filtering-log-messages#fully-dynamic-filtering 时,我尝试依赖 WhenMethodFilter
,它接受 lambda 回调 ShouldLogEvent
。然而,为了对回调 lambda 进行单元测试,我需要在生成配置的 class 中将其设为 public,这并不理想——我讨厌仅仅为了测试而制作方法 public。
private void ReconfigureNlog(object state)
{
var nlogConfig = ConstructNlogConfiguration();
foreach (var rule in nlogConfig.LoggingRules)
{
rule.Filters.Add(new WhenMethodFilter(ShouldLogEvent));
}
// TODO: maybe no need to reconfigure every time but just modify filter reference?
NLog.Web.NLogBuilder.ConfigureNLog(nlogConfig);
}
另一种方法是继承 Filter
基础 class 并尝试用单元测试覆盖它。但是它没有 public 接口的问题:
internal FilterResult GetFilterResult(LogEventInfo logEvent)
protected abstract FilterResult Check(LogEventInfo logEvent);
这使得它也不可测试,除非我自己制作方法 public。尽管这似乎是一个小问题,但我很好奇是否有更好的方法。从我的角度来看,将 GetFilterResult
设置为 internal 是绝对没有必要的,尽管它有点遵循最佳设计实践。想法?
更新1.待测class代码:
public class TenancyLogFilter: Filter
{
private readonly ITenancyLoggingConfiguration _loggingConfigurationConfig;
private readonly IHttpContextAccessor _httpContextAccessor;
public TenancyLogFilter(ITenancyLoggingConfiguration loggingConfigurationConfig, IHttpContextAccessor httpContextAccessor)
{
_loggingConfigurationConfig = loggingConfigurationConfig;
_httpContextAccessor = httpContextAccessor;
}
protected override FilterResult Check(LogEventInfo logEvent)
{
var result = FilterResult.Neutral;
if (CanEmitEvent(logEvent.Level))
{
result = FilterResult.Log;
}
return result;
}
private LogLevel GetMinLogLevel()
{
var level = LogLevel.Trace;
if (!_loggingConfigurationConfig.TenantMinLoggingLevel.Any())
return level;
var context = _httpContextAccessor?.HttpContext;
if (context == null)
return level;
if (context.Request.Headers.TryGetValue(CustomHeaders.TenantId, out var tenantIdHeaders))
{
var currentTenant = tenantIdHeaders.First();
if (_loggingConfigurationConfig.TenantMinLoggingLevel.ContainsKey(currentTenant))
{
level = _loggingConfigurationConfig.TenantMinLoggingLevel[currentTenant];
}
}
return level;
}
private bool CanEmitEvent(LogLevel currentLogLevel)
{
return currentLogLevel >= GetMinLogLevel();
}
}
通常丑陋的技巧是创建方法 internal
,然后将其添加到主项目中的 AssemblyInfo.cs:
using System;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleToAttribute("Tenancy.UnitTests")]
然后单元测试项目Tenancy.UnitTests
将被允许对主项目的内部方法进行单元测试。
出于测试 class 的目的,您可以从主题 class 派生以公开调用被测成员所需的内容,因为目标成员受到保护。
无法 change/access 内部成员,但在这种情况下,源显示它是一个简单的实现
/// <summary>
/// Gets the result of evaluating filter against given log event.
/// </summary>
/// <param name="logEvent">The log event.</param>
/// <returns>Filter result.</returns>
internal FilterResult GetFilterResult(LogEventInfo logEvent)
{
return Check(logEvent);
}
从那里开始,只需要提供必要的依赖项即可完成测试。
例如。
[TestClass]
public class TenancyLogFilterTests {
[TestMethod]
public void Should_Log_LogEvent() {
//Arrange
string expectedId = Guid.NewGuid().ToString();
LogLevel expectedLevel = LogLevel.Error;
FilterResult expected = FilterResult.Log;
var context = new DefaultHttpContext();
context.Request.Headers.Add(CustomHeaders.TenantId, expectedId);
var accessor = Mock.Of<IHttpContextAccessor>(_ => _.HttpContext == context);
var level = new Dictionary<string, LogLevel> {
{ expectedId, expectedLevel }
};
var config = Mock.Of<ITenancyLoggingConfiguration>(_ => _.TenantMinLoggingLevel == level);
var subject = new TestTenancyLogFilter(config, accessor);
var info = new LogEventInfo { Level = expectedLevel };
//Act
FilterResult actual = subject.GetFilterResult(info);
//Assert - FluentAssertions
actual.Should().Be(expected);
}
class TestTenancyLogFilter : TenancyLogFilter {
public TestTenancyLogFilter(ITenancyLoggingConfiguration loggingConfigurationConfig, IHttpContextAccessor httpContextAccessor)
: base(loggingConfigurationConfig, httpContextAccessor) { }
public FilterResult GetFilterResult(LogEventInfo logEvent) {
return Check(logEvent);
}
}
}
这样可以隔离测试,也可以解决外部第 3 方依赖项提供的限制。
实际过滤器保持不变,无需暴露任何额外内容。
从您的原始代码示例中,请注意,当遇到这样的阻塞问题时,应将其视为设计问题和提取服务抽象的标志。
例如
public interface ILogEventAssessor {
FilterResult GetFilterResult(LogEventInfo logEvent);
}
其实现将封装在自定义 TenancyLogFilter
过滤器中完成的操作,并注入目标 class
private readonly ILogEventAssessor service;
//...assumed injected service
private void ReconfigureNlog(object state) {
var nlogConfig = ConstructNlogConfiguration();
foreach (var rule in nlogConfig.LoggingRules) {
rule.Filters.Add(new WhenMethodFilter(ShouldLogEvent));
}
// TODO: maybe no need to reconfigure every time but just modify filter reference?
NLog.Web.NLogBuilder.ConfigureNLog(nlogConfig);
}
private FilterResult ShouldLogEvent(LogEventInfo logEvent) {
return service.GetFilterResult(logEvent);
}
//...
现在真的不需要测试第 3 方过滤器来验证您的逻辑了。
您可以测试您的 ILogEventAssessor
实施以单独验证您的自定义逻辑。