32

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?

Steve Hibbert
  • 2,045
  • 4
  • 30
  • 49
  • 3
    We normally inject `ILogger` to controller. What is the reason behind injecting `ILoggerFactory`? – Win Jul 20 '17 at 17:33
  • I can give it a try. Will it not still use the extension method and therefore still fail? – Steve Hibbert Jul 20 '17 at 17:38
  • 1
    It works! If I change the injected object to ILogger, I can mock the logger with "var mockLogger = new Mock>();" and then pass mockLogger.Object into the testing controller. If you @Win post an answer, I would be happy to accept. – Steve Hibbert Jul 20 '17 at 17:45
  • 1
    Since the question was marked as duplicate *although [the duplicate answer](https://stackoverflow.com/a/562187/296861) is a dead end*, I could not answer anymore. I'm glad that it solves the problem. – Win Jul 20 '17 at 17:54
  • @stuartd - May this question be opened for Win to submit an answer that I can accept, please? – Steve Hibbert Jul 20 '17 at 21:14
  • @SteveHibbert no problem – stuartd Jul 20 '17 at 21:16
  • @Win - OK, if you submit an answer, I will accept. Much obliged for your help. – Steve Hibbert Jul 21 '17 at 07:51
  • See my workaround for Moq here: https://stackoverflow.com/a/54887887/126520 – Cankut Feb 26 '19 at 14:43
  • In my case, I wasn't testing a WebAPI controller. The only constructor required an `ILoggerFactory`. – Daniel Przybylski Dec 16 '21 at 23:11

4 Answers4

79

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.

Benjamin
  • 1,983
  • 23
  • 33
  • 1
    Great answer! Exactly what is needed for testing purposes! – Vertigo Oct 16 '19 at 11:01
  • 2
    This should be the accepted answer, exactly how this should be done for testing purposes! – Rob Oct 28 '19 at 09:40
  • This got me through testing as well, should be accepted – Zakk Diaz Mar 06 '20 at 22:04
  • 4
    Can we verify the information that was logged to a `NullLogger` instance or an `ILogger` created by a `NullLoggerFactory` instance? That's one of the benefits of using a mocked `ILogger`, and I'd had to lose that. – devklick Mar 02 '21 at 13:34
  • If this doesn't allow us to validate the calls to the logger, then this really isn't an answer. [This answer](https://stackoverflow.com/a/58804614/1195056) provides a vastly superior approach that continues to allow for actual testing of all dependencies. – krillgar Feb 15 '23 at 12:47
20

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;
}
granadaCoder
  • 26,328
  • 10
  • 113
  • 146
Ryannet
  • 345
  • 2
  • 10
  • 1
    So I just found 2 exceptions in my code using this method instead of NullLoggerFactory. I had some code that looks like this: if (this.Logger.IsEnabled(LogLevel.Debug)) { /* do something */ } So this is my preferred approach now. Thanks Ryannet. Not sure why no one has seen this before. I'm adding a small tidbit to your answer, I hope thats ok. And the only way I could "get to" (aka "test") the code inside the if (this.Logger.IsEnabled(LogLevel.Debug)) block was via this method. Aka, with NullLoggerFactory , some code as not able to be reached – granadaCoder Jan 21 '20 at 17:54
  • 1
    I'm back to this answer! For future readers, one of the keys to this is that the method Ryannet mocks seems to be the ONLY concrete/real method. (as opposed to any static-extension methods where the extensions methods (which again, are static)...are NOT easily mocked. You can see this at this url. Note most of us are probably using the extension methods not not realizing it (LogInformation, LogError, etc). Here is the link : https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger.log?view=dotnet-plat-ext-5.0 – granadaCoder Mar 11 '21 at 20:14
  • I made another edit. I hope it adds to the answer. This is quite a little puzzle. – granadaCoder Mar 11 '21 at 20:35
  • At least in `Moq`, you're not able to mock Extension Methods. The `ILogger` methods you mock above aren't extension methods either. – Dave Black Jan 25 '23 at 16:29
9

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

Mark
  • 551
  • 6
  • 13
0

For anyone that simply wants to provide some logger so that you can debug your code via the console, you can create a simple ILoggerFactory like so:

var loggerFactory = LoggerFactory.Create(c => c
       .AddConsole()
       .SetMinimumLevel(LogLevel.Debug)
       );

//'inject' into your services
var myServiceToTest = new MyService(loggerFactory);

//or as a ILogger<T>, if thats how you're injecting
var myServiceToTest2 = new MyService(loggerFactory.CreateLogger<MyService>());

myServiceToTest.Execute();
//logger methods called in Execute should show in your IDE output console

DLeh
  • 23,806
  • 16
  • 84
  • 128