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