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?
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.
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());
});
Example with a Callback, tested with Moq 4.14.5. More Informations available on this Github Issue
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 });
}));
This is a case where a test double class may be easier than Moq. It's a little more work to create, but then you can re-use it forever and it's easier to read and work with than a Moq callback. (I like Moq, but not when there's an easier way.)
For most use cases this will work as-is, or you can tweak it.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
public class LoggerDouble<T> : ILogger, ILogger<T>
{
public List<LogEntry> LogEntries { get; } = new List<LogEntry>();
// Add more of these if they make life easier.
public IEnumerable<LogEntry> InformationEntries =>
LogEntries.Where(e => e.LogLevel == LogLevel.Information);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
LogEntries.Add(new LogEntry(logLevel, eventId, state, exception));
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
return new LoggingScope();
}
public class LoggingScope : IDisposable
{
public void Dispose()
{
}
}
}
public class LogEntry
{
public LogEntry(LogLevel logLevel, EventId eventId, object state, Exception exception)
{
LogLevel = logLevel;
EventId = eventId;
State = state;
Exception = exception;
}
public LogLevel LogLevel { get; }
public EventId EventId { get; }
public object State { get; }
public Exception Exception { get; }
}
Create an instance and inject it into your test class as the logger. Then you can look at the objects in the LogEntries collection to see what got logged.
The type of State will typically be FormattedLogValues, but you can call State.ToString() and just get the string value.
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