2

TL;DR:

As mocking open generic types isn't possible with Moq, creating actual mock is inescapable. Here's the bare minimal mock I ended up using:

internal interface ILoggerMock {
    public List<(LogLevel logLevel, string msg)> Logs { get; }
}

public class LoggerMock<T> : ILoggerMock, ILogger<T> {
    public List<(LogLevel logLevel, string msg)> Logs { get; } = new();
    public IDisposable BeginScope<TState>(TState state) => throw new NotImplementedException();
    public bool IsEnabled(LogLevel logLevel) => throw new NotImplementedException();
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) {
        var formattedLogMessage = formatter(state, exception);
        Logs.Add((logLevel, formattedLogMessage));
    }
}

With this registration:

var myServiceProvider = new ServiceCollection()
    // ... more registrations as needed ...
    .AddSingleton(typeof(ILogger<>), typeof(LoggerMock<>))
    // ... more registrations as needed ...
    .BuildServiceProvider()

_loggerMock = (ILoggerMock) myServiceProvider.GetService(typeof(ILogger<Answer>));

The original question:

Each class gets injected with ILogger<T> where T is each class, e.g.:

public class Question {
    public Question(ILogger<Question> logger) { /* ... */ }
}

public class Answer {
    public Answer(ILogger<Answer> logger) { /* ... */ }
}

But then it means that I must create a mock for each and every ILogger<T> consumer:

var questionLoggerMock = new Mock<ILogger<Question>>();
var answerLoggerMock = new Mock<ILogger<Answer>>();

var services = new ServiceCollection()
    .AddScoped<ILogger<Question>>(_ => questionLoggerMock.Object)
    .AddScoped<ILogger<Answer>>(_ => answerLoggerMock.Object)
    /*
    .
    ... all other ILogger consumers...
    .
    */

I tried many tricks, such as .AddSingleton<ILogger<object>>(_ => omniLoggerMock.Object) or .AddSingleton(typeof(ILogger<>), _ => omniLoggerMock.Object), but none seem to work (if compiled, they fail in runtime).

Is there a simple solution to that, or am I doomed to create a new mock for each and every generic type?

Tar
  • 8,529
  • 9
  • 56
  • 127
  • 2
    Why are you relying on a ServiceCollection for testing at all? – David L Jul 26 '22 at 17:16
  • I think @DavidL raised a fair question here. – Steven Jul 26 '22 at 19:39
  • You might also want to consider letting `Question` and `Answer` depend on the non-generic `ILogger` instead of the generic `ILogger` interface. This simplifies the problem during testing. Only downside is that MS.DI makes it pretty difficult to still inject a `Logger` into a non-generic `ILogger`, because it doesn't support context-based injection. Other DI containers might have better support on this. – Steven Jul 26 '22 at 19:41
  • @DavidL rather than? – Tar Aug 10 '22 at 07:06

1 Answers1

4

If you don't need to test/mock the calls, the easiest way is to use a NullLogger

services.Add(
    ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(NullLogger<>)));

If you need the mocking part because you need to see if you're logging the correct thing, you can do it like this:


var mocker = new Mock<ILogger>();
services.Add(
    ServiceDescriptor.Singleton(typeof(ILogger), mocker.Object));
services.Add(
    ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(ProxyLogger<>)));

public class ProxyLogger<T>: ILogger<T>{
    private readonly ILogger proxee;

    public ProxyLogger(ILogger proxee)
    {
        this.proxee = proxee;
    }

    public IDisposable BeginScope<TState>(TState state) 
        => proxee.BeginScope<TState>(state);

    public bool IsEnabled(LogLevel logLevel)
        => proxee.IsEnabled(logLevel);

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        => proxee.Log(logLevel, eventId, state, exception, formatter);
}

kalleguld
  • 371
  • 3
  • 9