
How To Test Logging In ASP.NET Core
In some cases, like analysis of problems in production, logging is really essential. Even you would like to test that your logging really works. So in this blog we are going to learn how to test logging in ASP.NET core:
Let’s take the following function:
Public void DoSomething(int input)
{
_logger.LogInformation(“Doing something…”);
try
{
// do something that might result in an exception
var result = 10 / input;
}
catch (Exception ex)
{
// swallow but log the exception
_logger.LogError(ex, “An error occurred doing something.”, input);
}
public void DoSomething(int input)
{
_logger.LogInformation(“Doing something…”);
try
{
// do something that might result in an exception
var result = 10 / input;
}
catch (Exception ex)
{
// swallow but log the exception
_logger.LogError(ex, “An error occurred doing something.”, input);
}
}
We can assume that with an occurrence of this exception, it’s costing the company good amount of money. Hence it is good to know about it to solve a problem. From a developer’s point of view, the LogError method has the business Importance. And to be sure about it’s working right, there must be automated tests.
Now, consider how to test this code when the _logger case in question is an ASP.NET Core ILogger<T> type, input like as it is:
public class SomeService
{
private readonly ILogger<SomeService> _logger;
public SomeService(ILogger<SomeService> logger)
{
_logger = logger;
}
// methods
}
public class SomeService
{
private readonly ILogger<SomeService> _logger;
public SomeService(ILogger<SomeService> logger)
{
_logger = logger;
}
// methods
}
Now the question is how to test this scenario! You can’t call the method and then observe SomeService’s state to see if it is working. The only way to confirm in a unit test is to pass in your own implementation of ILogger<SomeService> into SomeService, and then find out if this instance was called by the DoSomething method. One way to do this is to use a mocking library like Moq, as shown below:
public void LogsErrorWhenInputIsZero()
{
var mockLogger = new Mock<ILogger<SomeService>>();
var someService = new SomeService(mockLogger.Object);
someService.DoSomething(0);
// Option 1: Try to verify the actual code that was called.
// Doesn’t work.
mockLogger.Verify(l => l.LogError(It.IsAny<Exception>(), It.IsAny<string>(), 0));
}
public void LogsErrorWhenInputIsZero()
{
var mockLogger = new Mock<ILogger<SomeService>>();
var someService = new SomeService(mockLogger.Object);
someService.DoSomething(0);
// Option 1: Try to verify the actual code that was called.
// Doesn’t work.
mockLogger.Verify(l => l.LogError(It.IsAny<Exception>(), It.IsAny<string>(), 0));
}
To mention, this approach fails as there is no LogError method on ILogger<T>, which is the LoggerExtension. This is the main reason why unit testing logging is difficult in ASP.NET Core.
A solution to this is to open a code for the logger extensions and find out what non-extension method is finally executed on ILogger. This would lead to a test like this one:
public void LogsErrorWhenInputIsZeroTake2()
{
var mockLogger = new Mock<ILogger<SomeService>>();
var someService = new SomeService(mockLogger.Object);
someService.DoSomething(0);
// Option 2: Look up what instance method the extension method actually calls:
//https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Abstractions/LoggerExtensions.cs#L342
// Mock the underlying call instead.
// Works but is ugly and brittle
mockLogger.Verify(l => l.Log(LogLevel.Error, 0, It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(),
It.IsAny<Func<object, Exception, string>>()));
}
public void LogsErrorWhenInputIsZeroTake2()
{
var mockLogger = new Mock<ILogger<SomeService>>();
var someService = new SomeService(mockLogger.Object);
someService.DoSomething(0);
// Option 2: Look up what instance method the extension method actually calls:
// https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Abstractions/LoggerExtensions.cs#L342
// Mock the underlying call instead.
// Works but is ugly and brittle
mockLogger.Verify(l => l.Log(LogLevel.Error, 0, It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(),
It.IsAny<Func<object, Exception, string>>()));
}
But you might have to mock calls that don’t even exist in the method we’re testing.
But every problem has a solution finder. So finally a trouble-shooter solved it by mentioning the need to create own version of ILogger<T> and give it with its own implementation of the method that has to be checked.
As instance methods are used before extensions methods, this would rather override the extension method in the test code. Thus, will appear the implementation:
public void LogsErrorWhenInputIsZeroTake3()
{
var fakeLogger = new FakeLogger();
var someService = new SomeService(fakeLogger);
someService.DoSomething(0);
// Option 3: Create your own instance of ILogger<T> that has a non-extension version of the method
// Doesn’t work, unless you change system under test to take in a FakeLogger (which is useless)
Assert.NotNull(FakeLogger.ProvidedException);
Assert.NotNull(FakeLogger.ProvidedMessage);
}
private class FakeLogger : ILogger<SomeService>
{
public static Exception ProvidedException { get; set; }
public static string ProvidedMessage { get; set; }
public static object[] ProvidedArgs { get; set; }
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel, EventId, TState state, Exception, Func<TState, Exception, string> formatter)
{
}
public void LogError(Exception ex, string message, params object[] args)
{
ProvidedException = ex;
ProvidedMessage = message;
ProvidedArgs = args;
}
}
public void LogsErrorWhenInputIsZeroTake3()
{
var fakeLogger = new FakeLogger();
var someService = new SomeService(fakeLogger);
someService.DoSomething(0);
// Option 3: Create your own instance of ILogger<T> that has a non-extension version of the method
// Doesn’t work, unless you change system under test to take in a FakeLogger (which is useless)
Assert.NotNull(FakeLogger.ProvidedException);
Assert.NotNull(FakeLogger.ProvidedMessage);
}
private class FakeLogger : ILogger<SomeService>
{
public static Exception ProvidedException { get; set; }
public static string ProvidedMessage { get; set; }
public static object[] ProvidedArgs { get; set; }
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception, Func<TState, Exception, string> formatter)
{
}
public void LogError(Exception ex, string message, params object[] args)
{
ProvidedException = ex;
ProvidedMessage = message;
ProvidedArgs = args;
}
}
Even now you could assume that DoSomething is still calling the method LogError on ILogger<T>, Yet the extension method will be used because ILogger<T> doesn’t have an instance method that it could match. The only solution is to change the SomeService method to accept a FakeLogger, instead of an ILogger<T>, but even that doesn’t come as a real solution.
So what could be done?
The actual problem is we are dependent on types which are outside our control, and the types are using static methods which can’t be decoupled in tests. To attain loose coupling in our applications, we have to bring together dependencies on specific implementations by working with interfaces that can be controlled. This is a segment of the Interface Segregation Principle (ISP).
Now, some developers will counter because it’s not an implementation and even its part of the framework that we depend upon. But to accept the fact that, ILogger<T> isn’t an implementation, it’s only a use of extension methods and makes testing difficult. The best way to address the problem is with the use of an adapter.
You can start with the minimal interface which your client code needs. You don’t need to create one adapter for all but with every variation of method found on LoggerExtensions. Most applications won’t need all of them, so include the ones you require.
public interface ILoggerAdapter<T>
{
// add just the logger methods your app uses
void LogInformation(string message);
void LogError(Exception ex, string message, params object[] args);
}
public interface ILoggerAdapter<T>
{
// add just the logger methods your app uses
void LogInformation(string message);
void LogError(Exception ex, string message, params object[] args);
}
You can implement the adapter easily by passing in the implementation type it’s using. In this case, the ILogger<T> type and its extension methods:
public class LoggerAdapter<T> : ILoggerAdapter<T>
{
private readonly ILogger<T> _logger;
public LoggerAdapter(ILogger<T> logger)
{
_logger = logger;
}
public void LogError(Exception ex, string message, params object[] args)
{
_logger.LogError(ex, message, args);
}
public void LogInformation(string message)
{
_logger.LogInformation(message);
}
}
public class LoggerAdapter<T> : ILoggerAdapter<T>
{
private readonly ILogger<T> _logger;
public LoggerAdapter(ILogger<T> logger)
{
_logger = logger;
}
public void LogError(Exception ex, string message, params object[] args)
{
_logger.LogError(ex, message, args);
}
public void LogInformation(string message)
{
_logger.LogInformation(message);
}
}
At this point, you refract your service to remove the ILogger<T> dependency and instead use an ILoggerAdapter<T>. And next writing a text to verify that the error is logged properly becomes easy:
public void LogsErrorWhenInputIsZero()
{
var mockLogger = new Mock<ILoggerAdapter<SomeOtherService>>();
var someOtherService = new SomeOtherService(mockLogger.Object);
someOtherService.DoSomething(0);
mockLogger.Verify(l => l.LogError(It.IsAny<Exception>(), It.IsAny<string>(), 0));
}
public void LogsErrorWhenInputIsZero()
{
var mockLogger = new Mock<ILoggerAdapter<SomeOtherService>>();
var someOtherService = new SomeOtherService(mockLogger.Object);
someOtherService.DoSomething(0);
mockLogger.Verify(l => l.LogError(It.IsAny<Exception>(), It.IsAny<string>(), 0));
}
With this we conclude.
Keep coding!
If you want to enhance yourself in Dot Net and improve yourself through Dot NET training program; our institute, CRB Tech Solutions would be of great help and support. We offer a well-structured program for the best Dot Net Course.
Stay connected to CRB Tech for your technical up-gradation and to remain updated with all the happenings in the world of Dot Net..