自定义 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将被允许对主项目的内部方法进行单元测试。

另见 https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.internalsvisibletoattribute

出于测试 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);
}

Source

从那里开始,只需要提供必要的依赖项即可完成测试。

例如。

[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 实施以单独验证您的自定义逻辑。