Reputation: 2165
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?
Upvotes: 36
Views: 24591
Reputation: 24385
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
Upvotes: 0
Reputation: 395
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;
}
Upvotes: 25
Reputation: 2103
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.
Upvotes: 88
Reputation: 599
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
Upvotes: 9