18
votes

I am trying to write some unit tests for controller actions. To do that, I am using XUnit and Moq. The controllers have an ILoggerFactory injected in the constructor. How does one Moq this up for testing?

I have tried mocking a Logger for the controller class, and then mocking up CreateLogger to return the mock Logger, but I keep getting various test runtime NullReferenceExceptions when the LogInformation() function is called.

        //   Logger that yields only disappointment...          
        var mockLogger = new Mock<ILogger<JwtController>>();
        mockLogger.Setup(ml => ml.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
        var mockLoggerFactory = new Mock<ILoggerFactory>();
        mockLoggerFactory.Setup(mlf => mlf.CreateLogger("JwtController")).Returns(mockLogger.Object);

I assume the problem is that LogInformation is being called, and this is an extension method, so how to moq that?

3
We normally inject ILogger<SampleController> to controller. What is the reason behind injecting ILoggerFactory? - Win
I can give it a try. Will it not still use the extension method and therefore still fail? - Steve Hibbert
It works! If I change the injected object to ILogger<MyController>, I can mock the logger with "var mockLogger = new Mock<ILogger<MyController>>();" and then pass mockLogger.Object into the testing controller. If you @Win post an answer, I would be happy to accept. - Steve Hibbert
Since the question was marked as duplicate although the duplicate answer is a dead end, I could not answer anymore. I'm glad that it solves the problem. - Win
@stuartd - May this question be opened for Win to submit an answer that I can accept, please? - Steve Hibbert

3 Answers

44
votes

For what it's worth: instead of mocking an ILoggerFactory, you could also pass an instance of NullLoggerFactory. This NullLoggerFactory will return instances of NullLogger. According to the docs, this is a:

Minimalistic logger that does nothing.

9
votes

I just mock the ILogger extension methods as below, and use a value function in the ILoggerFactory setup that returns the Mock ILogger object.

var mockLogger = new Mock<ILogger<[YOUR_CLASS_TYPE]>>();
mockLogger.Setup(
    m => m.Log(
        LogLevel.Information,
        It.IsAny<EventId>(),
        It.IsAny<object>(),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()));

var mockLoggerFactory = new Mock<ILoggerFactory>();
mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(() => mockLogger.Object);

This will return your mocked Logger and you can verify any calls on it. No need to write wrappers or helpers.

You can even mock the IsEnabled, which is necessary for some code that leverages that functionality.

        mockLogger.Setup(
            m => m.IsEnabled(
                Microsoft.Extensions.Logging.LogLevel.Debug)).Returns(true);

Because there is only one method to mock, (and that you have to call), below shows the logging call you (might) use to have everything pointed to the this exact method.

 catch (ArgumentOutOfRangeException argEx)
{
    // this.logger.LogError(argEx, argEx.Message); /* << this is what you would like to do, BUT it is an extension method, NOT (easily) mockable */
    Func<object, Exception, string> returnAnEmptyStringFunc = (a, b) => string.Empty;
    this.logger.Log(LogLevel.Error, ushort.MaxValue, argEx.Message, argEx, returnAnEmptyStringFunc);
    throw argEx;
}
8
votes

For anybody needing an answer to this question, rather than a work around, I extended the work of this article:

https://ardalis.com/testing-logging-in-aspnet-core

Wrap any part of the logging framework that you use, which is a good idea anyway. Start with your own logging interface:

public interface ILog<T>
{
    void LogError(Exception ex, string message, params object[] args);
    void LogInformation(string message, params object[] args);
}

Then add the implementation to pass through calls to the framework:

public class Log<T> : ILog<T>
{
    private readonly ILogger<T> logger;

    public Log(ILogger<T> logger)
    {
        this.logger = logger;
    }

    public void LogError(Exception ex, string message, params object[] args) => this.logger.LogError(ex, message, args);
    public void LogInformation(string message, params object[] args) => this.logger.LogInformation(message, args);
}

Moving on, add an interface for wrapping the logger factory:

public interface ILogFactory
{
    ILog<T> CreateLog<T>();
}

And the implementation:

public class LogFactory : ILogFactory
{
    private readonly ILoggerFactory loggerFactory;

    public LogFactory()
    {
        this.loggerFactory = new LoggerFactory();
    }

    public ILog<T> CreateLog<T>() => new Log<T>(new Logger<T>(this.loggerFactory));
}

These are the only places where you should refer to the Microsoft.Extensions.Logging namespace. Elsewhere, use your ILog<T> instead of ILogger<T> and your ILogFactory instead of ILoggerFactory. Where you would normally dependency inject LoggerFactory, instead inject the wrapper:

IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ILogFactory>(new LogFactory());

In your main code you can retrieve this LogFactory and create your specific Log<T> for your class:

public class MyClass
{
    public void MyMethod(IServiceCollection serviceCollection)
    {
        var serviceProvider = serviceCollection.BuildServiceProvider();
        var logFactory = this.serviceProvider.GetRequiredService<ILogFactory>();
        var log = logFactory.CreateLog<ServiceApplication>();
        log.LogInformation("Hello, World!");
    }
}

You can imagine changing the parameter of MyMethod from IServiceCollection to ILogFactory or an ILog<MyClass> as required. And - the whole point is - you can now mock the above code with:

[Fact]
public void Test()
{
    IServiceCollection serviceCollection = new ServiceCollection();
    var mockLog = new Mock<ILog<MyClass>>();
    var mockLogFactory = new Mock<ILogFactory>();
    mockLogFactory.Setup(f => f.CreateLog<MyClass>()).Returns(mockLog.Object);
    serviceCollection.AddSingleton<ILogFactory>(mockLogFactory.Object);

    var myClass = new MyClass();
    myClass.MyMethod(serviceCollection);

    mockLog.Verify(l => l.LogInformation("Hello, World!"), Times.Once);
}

"Depending on types you don’t control throughout your application adds coupling and frequently becomes a source of problems and technical debt." - Steve Smith