How to unit test with ILogger in ASP.NET Core

若如初见. 提交于 2019-11-30 02:33:07

Just mock it as well as any other dependency:

var mock = new Mock<ILogger<BlogController>>();
ILogger<BlogController> logger = mock.Object;

//or use this short equivalent 
logger = Mock.Of<ILogger<BlogController>>()

var controller = new BlogController(logger);

You probably will need to install Microsoft.Extensions.Logging.Abstractions package to use ILogger<T>.

Moreover you can create a real logger:

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();
Amir Shitrit

Actually, I've found Microsoft.Extensions.Logging.Abstractions.NullLogger<> which looks like a perfect solution.

Use a custom logger that uses ITestOutputHelper (from xunit) to capture output and logs. The following is a small sample that only writes the state to the output.

public class XunitLogger<T> : ILogger<T>, IDisposable
{
    private ITestOutputHelper _output;

    public XunitLogger(ITestOutputHelper output)
    {
        _output = output;
    }
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        _output.WriteLine(state.ToString());
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return this;
    }

    public void Dispose()
    {
    }
}

Use it in your unittests like

public class BlogControllerTest
{
  private XunitLogger<BlogController> _logger;

  public BlogControllerTest(ITestOutputHelper output){
    _logger = new XunitLogger<BlogController>(output);
  }

  [Fact]
  public void Index_ReturnAViewResult_WithAListOfBlog()
  {
    var mockRepo = new Mock<IDAO<Blog>>();
    mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
    var controller = new BlogController(_logger,mockRepo.Object);
    // rest
  }
}

Already mentioned you can mock it as any other interface.

var logger = new Mock<ILogger<QueuedHostedService>>();

So far so good.

Nice thing is that you can use Moq to verify that certain calls have been performed. For instance here I check that the log has been called with a particular Exception.

logger.Verify(m => m.Log(It.Is<LogLevel>(l => l == LogLevel.Information), 0,
            It.IsAny<object>(), It.IsAny<TaskCanceledException>(), It.IsAny<Func<object, Exception, string>>()));

When using Verify the point is to do it against the real Log method from the ILooger interface and not the extension methods.

Adding my 2 cents, This is a helper extension method typically put in a static helper class:

static class MockHelper
{
    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    {
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    }

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    {
        return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    }

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    {
        mock.Verify(Verify<T>(level), times);
    }
}

Then, you use it like this:

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

And of course you can easily extend it to mock any expectation (i.e. expection, message, etc …)

It is easy as other answers suggest to pass mock ILogger, but it suddenly becomes much more problematic to verify that calls actually were made to logger. The reason is that most calls do not actually belong to the ILogger interface itself.

So the most calls are extension methods that call the only Log method of the interface. The reason it seems is that it's way easier to make implementation of the interface if you have just one and not many overloads that boils down to same method.

The drawback is of course that it is suddenly much harder to verify that a call has been made since the call you should verify is very different from the call that you made. There are some different approaches to work around this, and I have found that custom extension methods for mocking framework will make it easiest to write.

Here is an example of a method that I have made to work with NSubstitute:

public static class LoggerTestingExtensions
{
    public static void LogError(this ILogger logger, string message)
    {
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    }

}

And this is how it can be used:

_logger.Received(1).LogError("Something bad happened");   

It looks exactly as if you used the method directly, the trick here is that our extension method gets priority because it's "closer" in namespaces than the original one, so it will be used instead.

It does not give unfortunately 100% what we want, namely error messages will not be as good, since we don't check directly on a string but rather on a lambda that involves the string, but 95% is better than nothing :) Additionally this approach will make the test code

P.S. For Moq one can use the approach of writing an extension method for the Mock<ILogger<T>> that does Verify to achieve similar results.

And when using StructureMap / Lamar:

var c = new Container(_ =>
{
    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});

Docs:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!