Verifying logger calls with Moq
Verifying whether a call to ILogger
has been made from a unit test is somewhat tricky because we're more often calling extension methods than instance methods, and those can't be directly verified. We need to verify the call to the underlying instance method.
Here's what a typical logger call in our code looks like:
logger.LogInformation(message);
However, LogInformation
is an extension method:
public static void LogInformation(
this ILogger logger,
string? message,
params object?[] args
);
Eventually, the following instance method is called:
void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter
);
Fortunately, this is the single instance logging method, which is being called by all extension method we might actually call. So, this is the method to verify in our tests. We'll use Moq for that. For the sample logger call above, we're only interested in the log level and message:
var loggerMock = new Mock<ILogger<Service>>();
var service = new Service(loggerMock.Object);
service.LogMessage("Hello, world!");
loggerMock.Verify(logger =>
logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((value, _) =>
value.ToString()!.Contains("Hello, world!")
),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
)
);
We can simplify the verification call a bit if we create an extension method for it:
public static void VerifyLog<T>(
this Mock<ILogger<T>> loggerMock,
LogLevel logLevel,
string message
)
{
loggerMock.Verify(logger =>
logger.Log(
logLevel,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((value, _) => value.ToString()!.Contains(message)),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
)
);
}
And call that one from the tests instead:
var loggerMock = new Mock<ILogger<Service>>();
var service = new Service(loggerMock.Object);
service.LogMessage("Hello, world!");
loggerMock.VerifyLog(
LogLevel.Information,
"Hello, world!"
);
We can further expand the extension method to optionally check the event ID and the exception included with the log:
public static void VerifyLog<T>(
this Mock<ILogger<T>> loggerMock,
LogLevel logLevel,
string message,
int? eventId = null,
Func<Exception, bool>? exceptionPredicate = null
)
{
loggerMock.Verify(logger =>
logger.Log(
logLevel,
It.Is<EventId>(e => !eventId.HasValue || eventId.Value == e.Id),
It.Is<It.IsAnyType>((value, _) => value.ToString()!.Contains(message)),
It.Is<Exception>(e =>
exceptionPredicate == null || exceptionPredicate(e)
),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
)
);
}
This allows us to use it with other logging calls:
// verify event id
loggerMock.VerifyLog(LogLevel.Information, "Event message", eventId: 1);
// verify exception
loggerMock.VerifyLog(
LogLevel.Error,
"An error occurred",
exceptionPredicate: e => e is NullReferenceException
);
You can see these calls as part of unit tests in my GitHub repository. Feel free to run those tests and further modify the extension method for other use cases.
It's not common to have to verify logger calls in your unit tests, but you might still need to do it from time to time. Because logging methods are extension methods, they can't be verified directly. You need to find the underlying instance method and verify that one. The same approach can be used for verifying (or mocking) extension methods in other scenarios as well.