Юнит-тестирование пользовательского класса фильтров NLog - PullRequest
2 голосов
/ 02 ноября 2019

При реализации пользовательского фильтра NLog https://github.com/NLog/NLog/wiki/Filtering-log-messages#fully-dynamic-filtering я пытался положиться на WhenMethodFilter, который принимает лямбда-колбэк ShouldLogEvent. Однако для модульного тестирования этой лямбда-функции обратного вызова мне нужно сделать ее общедоступной в классе, который генерирует конфигурацию, что не идеально - я ненавижу делать методы общедоступными только ради тестирования.

    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 базовый класс и пытаться покрыть его юнит-тестами. Но проблема в том, что у него нет открытого интерфейса:

internal FilterResult GetFilterResult(LogEventInfo logEvent)
protected abstract FilterResult Check(LogEventInfo logEvent);

, что делает его также не тестируемым, если я не сделаю свои собственные методы публичными. Хотя это кажется небольшой проблемой, мне любопытно, есть ли лучший способ. С моей точки зрения, делать GetFilterResult внутренним абсолютно не нужно, хотя это своего рода следование лучшим практикам проектирования. Мысли?

Обновление 1. Код класса для тестирования:

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();
    }
}

Ответы [ 2 ]

2 голосов
/ 02 ноября 2019

В целях тестирования класса вы можете извлечь из предметного класса то, что необходимо для вызова тестируемого члена, поскольку целевой элемент защищен.

Нет способа изменить / получить доступ квнутренний член, но в этом случае источник показывает, что это простая реализация

/// <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);
        }
    }
}

Это позволяет изолировать тест, а также обходить ограничения, предоставляемые внешней зависимостью третьей стороны.

Фактический фильтр остается прежним без необходимости предоставлять что-либо дополнительное.

Из исходного кода кода обратите внимание, что при возникновении такой проблемы с блокировкой его следует рассматривать как проблему дизайнаи знак для извлечения сервисной абстракции.

Например

public interface ILogEventAssessor {
    FilterResult GetFilterResult(LogEventInfo logEvent);
}

, реализация которого будет включатьобъясните, что было сделано в пользовательском фильтре TenancyLogFilter и добавлено в целевой класс

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);
}

//...

Теперь действительно нет необходимости проверять сторонний фильтр для проверки вашей логики.

Вы можете проверить свою реализацию ILogEventAssessor, чтобы проверить свою собственную логику в отдельности.

1 голос
/ 02 ноября 2019

Обычно уродливым трюком является создание метода 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

...