I’ve been creating a new template solution for our ASP.NET Core projects. As I was writing some tests for an API controller, I hit a problem with mocking the ILogger<T>
interface. So I thought I would write a quick blog post about what I found, mainly so I won’t forget in the future!
I had a setup similar to the following code.
public class CatalogueController : Controller
{
private readonly ILogger<CatalogueController> _logger;
private readonly ICatalogueService _catalogueService;
public CatalogueController(ILogger<CatalogueController> logger, ICatalogueService catalogueService)
{
_logger = logger;
_catalogueService = catalogueService;
}
[HttpGet("api/catalogue")]
public async Task<IActionResult> GetActiveStockItemsAsync()
{
try
{
var stockItems = await _catalogueService.GetActiveStockItemsAsync();
return Ok(stockItems);
}
catch (Exception exception)
{
_logger.LogError("Error returning active stock catalogue items", exception);
return new StatusCodeResult((int)HttpStatusCode.InternalServerError);
}
}
public class CatalogueControllerTests
{
private readonly IFixture _fixture;
private readonly Mock<ILogger<CatalogueController>> _mockLogger;
private readonly Mock<ICatalogueService> _mockCatalogueService;
private readonly CatalogueController _catalogueController;
public CatalogueControllerTests()
{
_fixture = new Fixture();
_mockLogger = new Mock<ILogger<CatalogueController>>();
_mockCatalogueService = new Mock<ICatalogueService>();
_catalogueController = new CatalogueController(_mockLogger.Object, _mockCatalogueService.Object);
}
[Fact]
public async Task GetActiveStockItems_LogsErrorAndReturnsInternalServerError_When_ErroOnServer()
{
// Arrange
_mockCatalogueService.Setup(x => x.GetActiveStockItemsAsync()).Throws<Exception>();
// Act
var result = await _catalogueController.GetActiveStockItemsAsync();
// Assert
_mockLogger.Verify(x => x.LogError("Error returning active stock catalogue items", It.IsAny<Exception>()), Times.Once);
var errorResult = Assert.IsType<StatusCodeResult>(result);
Assert.Equal(500, errorResult.StatusCode);
}
}
When I ran my test expecting it to pass I got the following error.
Message: System.NotSupportedException : Invalid verify on an extension method: x => x.LogError("Error returning active stock catalogue items", new[] { It.IsAny<Exception>() })
It turns out LogInformation
, LogDebug
, LogError
, LogCritial
and LogTrace
are all extension methods. After a quick Google I came across this issue on GitHub with an explanation from Brennan Conroy as to why the ILogger
interface is so limited.
Right now the ILogger interface is very small and neat, if you want to make a logger you only need to implement 3 methods, why would we want to force someone to implement the 24 extra extension methods everytime they inherit from ILogger?
Solution 1
There is a method on the ILogger
interface which you can verify against, ILogger.Log
. Ultimately all the extension methods call this log method. So a quick change to the verify code in my unit test and I had a working test.
Unit Test - Solution 1
public class CatalogueControllerTests
{
private readonly IFixture _fixture;
private readonly Mock<ILogger<CatalogueController>> _mockLogger;
private readonly Mock<ICatalogueService> _mockCatalogueService;
private readonly CatalogueController _catalogueController;
public CatalogueControllerTests()
{
_fixture = new Fixture();
_mockLogger = new Mock<ILogger<CatalogueController>>();
_mockCatalogueService = new Mock<ICatalogueService>();
_catalogueController = new CatalogueController(_mockLogger.Object, _mockCatalogueService.Object);
}
[Fact]
public async Task GetActiveStockItems_LogsErrorAndReturnsInternalServerError_When_ErroOnServer()
{
// Arrange
_mockCatalogueService.Setup(x => x.GetActiveStockItemsAsync()).Throws<Exception>();
// Act
var result = await _catalogueController.GetActiveStockItemsAsync();
// Assert
_mockLogger.Verify(x => x.Log(LogLevel.Error, It.IsAny<EventId>(), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()), Times.Once);
var errorResult = Assert.IsType<StatusCodeResult>(result);
Assert.Equal(500, errorResult.StatusCode);
}
}
Solution 2
Another way to solve the problem I found on the same GitHub issue from Steve Smith. He’s written a blog post about the issues of unit testing the ILogger
and suggests creating an adapter for the default ILogger
.
public interface ILoggerAdapter<T>
{
void LogInformation(string message);
void LogError(Exception ex, string message, params object[] args);
...
}
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);
}
...
}
The LoggerAdapter
will need to be added to DI as well.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton(typeof(ILoggerAdapter<>), typeof(LoggerAdapter<>));
}
With this in place I could then change my code as follows.
public class CatalogueController : Controller
{
private readonly ILoggerAdapter<CatalogueController> _logger;
private readonly ICatalogueService _catalogueService;
public CatalogueController(ILoggerAdapter<CatalogueController> logger, ICatalogueService catalogueService)
{
_logger = logger;
_catalogueService = catalogueService;
}
[HttpGet("api/catalogue")]
public async Task<IActionResult> GetActiveStockItemsAsync()
{
try
{
var stockItems = await _catalogueService.GetActiveStockItemsAsync();
return Ok(stockItems);
}
catch (Exception exception)
{
_logger.LogError("Error returning active stock catalogue items", exception);
return new StatusCodeResult((int)HttpStatusCode.InternalServerError);
}
}
public class CatalogueControllerTests
{
private readonly IFixture _fixture;
private readonly Mock<ILoggerAdapter<CatalogueController>> _mockLogger;
private readonly Mock<ICatalogueService> _mockCatalogueService;
private readonly CatalogueController _catalogueController;
public CatalogueControllerTests()
{
_fixture = new Fixture();
_mockLogger = new Mock<ILoggerAdapter<CatalogueController>>();
_mockCatalogueService = new Mock<ICatalogueService>();
_catalogueController = new CatalogueController(_mockLogger.Object, _mockCatalogueService.Object);
}
[Fact]
public async Task GetActiveStockItems_LogsErrorAndReturnsInternalServerError_When_ErroOnServer()
{
// Arrange
_mockCatalogueService.Setup(x => x.GetActiveStockItemsAsync()).Throws<Exception>();
// Act
var result = await _catalogueController.GetActiveStockItemsAsync();
// Assert
_mockLogger.Verify(x => x.LogError("Error returning active stock catalogue items", It.IsAny<Exception>()), Times.Once);
var errorResult = Assert.IsType<StatusCodeResult>(result);
Assert.Equal(500, errorResult.StatusCode);
}
}
Wrapping up
To summarise, there are a couple of ways you can go about unit testing when ILogger
is involved.
The first is to verify against the Log
method, the downside here is that it may not seem very obvious why you are doing it this way. The second option is to wrap the logger with your own implementation. This allows you to mock and verify methods as normal. But the downside is having to write the extra code to achieve it.
Do you know of any other ways to test the ILogger
? If so let me know in the comments.