A testable logger adapter for Microsoft.Extensions.Logging.ILogger that enables easy mocking and verification of log calls using a compatible mocking framework. Provides explicit interface methods for all logging operations including structured logging, exceptions, EventIds, and scopes.
$ dotnet add package MockableLoggerA logger adapter for Microsoft.Extensions.Logging.ILogger<T> that enables easy mocking and verification of log calls in unit tests.
Microsoft's ILogger<T> interface uses extension methods for common logging operations (LogInformation, LogError, etc.). These extension methods cannot be directly mocked because they're not part of the interface contract. This makes it difficult to verify that specific log messages were written during unit tests.
For example, consider the following class:
public class TestClass
{
private readonly ILogger<TestClass> _logger;
public TestClass(ILogger<TestClass> logger)
{
_logger = logger;
}
public void DoLogInfoWithArgs(string name)
{
_logger.LogInformation("Info with {Name}", name);
}
}
This Unit Test using Moq will fail:
[Fact]
public void TestLogInformationExtension()
{
var mockLogger = new Mock<ILogger<TestClass>>();
var sut = new TestClass(mockLogger.Object);
sut.DoLogInfoWithArgs("name");
mockLogger.Verify(m => m.LogInformation("Info with {Name}", "name"), Times.Once);
}
System.NotSupportedException
Unsupported expression: m => m.LogInformation("Info with {Name}", new[] { "name" })
Extension methods (here: LoggerExtensions.LogInformation) may not be used in setup / verification expressions.
at Moq.Guard.IsOverridable(MethodInfo method, Expression expression) in /_/src/Moq/Guard.cs:line 87
The following Unit Test using NSubstitute has the same problem, but with a less-specific error message:
[Fact]
public void TestLogInformationExtension_NSubstitute()
{
var mockLogger = Substitute.For<ILogger<TestClass>>();
var sut = new TestClass(mockLogger);
sut.DoLogInfoWithArgs("name");
mockLogger.Received(1).LogInformation("Info with {Name}", "name");
}
NSubstitute.Exceptions.ReceivedCallsException
Expected to receive exactly 1 call matching:
Log<FormattedLogValues>(Information, 0, Info with name, <null>, Func<FormattedLogValues, Exception, String>)
Actually received no matching calls.
Received 1 non-matching call (non-matching arguments indicated with '*' characters):
Log<FormattedLogValues>(Information, 0, *Info with name*, <null>, Func<FormattedLogValues, Exception, String>)
JustMock Lite fails with:
Telerik.JustMock.Core.ElevatedMockingException
Cannot mock 'Microsoft.Extensions.Logging.LoggerExtensions'.
JustMock Lite can only mock interface members, virtual/abstract members in non-sealed classes, delegates and all members on classes derived from MarshalByRefObject on instances created with Mock.Create or Mock.CreateLike.
For any other scenario you need to use the full version of JustMock.
MockableLogger provides:
IMockableLogger - An interface with explicit method signatures for all logging operationsTestLoggerAdapter<T> - An adapter that implements ILogger<T> and routes calls to IMockableLogger [Fact]
public void TestLogInformationExtension()
{
var mockLogger = Substitute.For<IMockableLogger>();
var adapter = new TestLoggerAdapter<TestClass>(mockLogger);
var sut = new TestClass(adapter);
sut.DoLogInfoWithArgs("name");
mockLogger.Received(1).LogInformation("Info with {Name}", "name");
}
[Fact]
public void TestLogInformationExtension()
{
var mockLogger = new Mock<IMockableLogger>();
var adapter = new TestLoggerAdapter<TestClass>(mockLogger.Object);
var sut = new TestClass(adapter);
sut.DoLogInfoWithArgs("name");
mockLogger.Verify(m => m.LogInformation("Info with {Name}", "name"), Times.Once);
}
}
[Fact]
public void TestLogInformationExtension()
{
var mockLogger = Mock.Create<IMockableLogger>();
var adapter = new TestLoggerAdapter<TestClass>(mockLogger);
var sut = new TestClass(adapter);
sut.DoLogInfoWithArgs("name");
Mock.Assert(() => mockLogger.LogInformation("Info with {Name}", "name"));
}
# Install the package
dotnet add package MockableLogger
Microsoft provides FakeLogger<T> in the Microsoft.Extensions.Logging.Testing package, but it has different goals and trade-offs:
using Microsoft.Extensions.Logging.Testing;
[Fact]
public void MyMethod_LogsMessage_WithFakeLogger()
{
// Arrange
var fakeLogger = new FakeLogger<MyService>();
var service = new MyService(fakeLogger);
// Act
service.DoSomething();
// Assert - inspect the collector
var logRecord = Assert.Single(fakeLogger.Collector.GetSnapshot());
Assert.Equal(LogLevel.Information, logRecord.Level);
Assert.Equal("Operation completed successfully", logRecord.Message);
}
FakeLogger Characteristics:
MockableLogger Characteristics:
Use FakeLogger when:
Use MockableLogger when:
IsEnabled() returns false)