Reputation: 789
While implementing custom NLog filter https://github.com/NLog/NLog/wiki/Filtering-log-messages#fully-dynamic-filtering I tried to rely on WhenMethodFilter
, which accepts lambda callback ShouldLogEvent
. However to unit test that callback lambda I need to make it public in the class that generates the config, which is not ideal - I hate making methods public just for the sake of testing.
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);
}
Another approach would be to inherit Filter
base class and try to cover it with unit tests. But the issue that it does not have a public interface :
internal FilterResult GetFilterResult(LogEventInfo logEvent)
protected abstract FilterResult Check(LogEventInfo logEvent);
which makes it also not testable unless I make my own methods public. As much as this seems to be a small issue, I'm curious if there is a better way. From my perspective, making GetFilterResult
internal is absolutely unnecessary, although it kinda following best design practices. Thoughts?
Update 1. Code of the class to be tested:
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();
}
}
Upvotes: 2
Views: 228
Reputation: 247133
For the purpose of testing the class you can derive from the subject class to expose what is needed to invoke the member under test since the target member is protected.
There is no way to change/access the internal member, but in this case the source shows it to be a simple implementation
/// <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);
}
From there it is just a matter of providing the necessary dependencies that would allow the test to be exercised to completion.
For example.
[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);
}
}
}
This allows the test to be isolated and also works around the limitation provided by the external 3rd party dependency.
The actual filter remains as before without having to expose anything additional.
From your original code example, take note that when faced with blocking issue like that, it should be see as a design issue and a sign to extract a service abstraction.
For example
public interface ILogEventAssessor {
FilterResult GetFilterResult(LogEventInfo logEvent);
}
whose implementation would encapsulate what was done in the custom TenancyLogFilter
filter, and injected into the target 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);
}
//...
Now there really is no need to test the 3rd party filter to verify your logic.
You can test your ILogEventAssessor
implementation to verify your custom logic in isolation.
Upvotes: 2
Reputation: 19867
Usually the ugly trick is to make the method internal
and then add this to your AssemblyInfo.cs in main-project:
using System;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleToAttribute("Tenancy.UnitTests")]
Then the unit-test-project Tenancy.UnitTests
will be allowed to unit-test the internal-methods of the main-project.
Upvotes: 1