I have a class that receives an ILogger and I want to mock the LogInformation calls but this is an extension method. How do I make the appropiate setup call for this?
To mock an ILogger<T> object, we can use Moq library to instantiate a new Mock<ILogger<T>>() object. Make sure you have installed the latest Moq package (v4. 15.1 as of Nov. 16, 2020, which contains an update to support “nested” type matchers).
If you're using Moq >= 4.13, here is a way to mock ILogger
:
logger.Verify(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
You can change the It.IsAny<LogLevel>()
, It.IsAny<EventId>()
, and It.IsAny<Exception>()
stubs to be more specific, but using It.IsAnyType
is necessary because FormattedLogValues
is now internal
.
Reference: TState in ILogger.Log used to be object, now FormattedLogValues
Example with a Callback, tested with Moq 4.14.5. More Informations available on this Github Issue
PM> install-package Moq
var logger = new Mock<ILogger<ReplaceWithYourObject>>();
logger.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
.Callback(new InvocationAction(invocation =>
{
var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
var eventId = (EventId)invocation.Arguments[1]; // so I'm not sure you would ever want to actually use them
var state = invocation.Arguments[2];
var exception = (Exception)invocation.Arguments[3];
var formatter = invocation.Arguments[4];
var invokeMethod = formatter.GetType().GetMethod("Invoke");
var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });
}));
A complete generic helper class for UnitTesting
public static class LoggerHelper
{
public static Mock<ILogger<T>> GetLogger<T>()
{
var logger = new Mock<ILogger<T>>();
logger.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
.Callback(new InvocationAction(invocation =>
{
var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
var eventId = (EventId)invocation.Arguments[1]; // so I'm not sure you would ever want to actually use them
var state = invocation.Arguments[2];
var exception = (Exception)invocation.Arguments[3];
var formatter = invocation.Arguments[4];
var invokeMethod = formatter.GetType().GetMethod("Invoke");
var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });
Trace.WriteLine($"{logLevel} - {logMessage}");
}));
return logger;
}
}
ILogger is normally used thru extension methods, LogWarning, LogError, etc.
In my case I was interested in the LogWarning method which after looking at the code calls the Log method from ILogger. In order to mock it with Moq, this is what I ended up doing:
var list = new List<string>();
var logger = new Mock<ILogger>();
logger
.Setup(l => l.Log<FormattedLogValues>(LogLevel.Warning, It.IsAny<EventId>(), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<FormattedLogValues, Exception, string>>()))
.Callback(
delegate (LogLevel logLevel, EventId eventId, FormattedLogValues state, Exception exception, Func<FormattedLogValues, Exception, string> formatter)
{
list.Add(state.ToString());
});
In newer versions of .NET Core 3.0 this won't work. Because FormattedLogValues is an internal type. You need to update the moq version to at least:
`<PackageReference Include="Moq" Version="4.16.0" />`
After updating Moq
The workaround is like this:
var log = new List<string>();
var mockLogger = new Mock<ILogger>();
mockLogger.Setup(
l => l.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
.Callback((IInvocation invocation) =>
{
var logLevel = (LogLevel)invocation.Arguments[0];
var eventId = (EventId)invocation.Arguments[1];
var state = (IReadOnlyCollection<KeyValuePair<string, object>>)invocation.Arguments[2];
var exception = invocation.Arguments[3] as Exception;
var formatter = invocation.Arguments[4] as Delegate;
var formatterStr = formatter.DynamicInvoke(state, exception);
log.Add(
$"{logLevel} - {eventId.Id} - Testing - {formatterStr}");
});
Notice the special cast: (Func<It.IsAnyType, Exception, string>)It.IsAny<object>())
and also the IInvocation to handle the arguments.
This is how I workaround for Moq (v4.10.1) framework.
public static class TestHelper
{
public static Mock<ILogger<T>> GetMockedLoggerWithAutoSetup<T>()
{
var logger = new Mock<ILogger<T>>();
logger.Setup<object>(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<object>(),
It.IsAny<Exception>(),
It.IsAny<Func<object, Exception, string>>()));
return logger;
}
public static void VerifyLogMessage<T>(Mock<ILogger<T>> mockedLogger, LogLevel logLevel, Func<string, bool> predicate, Func<Times> times)
{
mockedLogger.Verify(x => x.Log(logLevel, 0, It.Is<object>(p => predicate(p.ToString())), null, It.IsAny<Func<object, Exception, string>>()), times);
}
}
--
public class Dummy
{
}
[Fact]
public void Should_Mock_Logger()
{
var logger = TestHelper.GetMockedLoggerWithAutoSetup<Dummy>();
logger.Object.LogInformation("test");
TestHelper.VerifyLogMessage<Dummy>(logger, LogLevel.Information, msg => msg == "test", Times.Once);
}
--
The thing is,
If I had chosen any other <TCustom>
than <object>
for logger.Setup()
, it would fail on Verify
step saying that 0 calls were made for x.Log<TCustom>
and showing a call made to x.Log<object>
. So I setup my generic logger to mock Log<object>(..)
method instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With