35

Given a class with a constructor signature of

public Foo(ILogger<Foo> logger) {
    // ...
}

that I want to test, I need some way to provide an ILogger<Foo> in the test. It's been asked before, but the only answer then was to set up a full-blown service registry, configure logging and resolve the logger from there. This seems very overkill to me.

Is there a simple way to provide an implementation of ILogger<T> for testing purposes?

Note: it doesn't have to actually log anything - just not blow up when the subject under test tries to log something.

Adrian Toman
  • 11,316
  • 5
  • 48
  • 62
Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • 2
    Why not just create an empty implementation of ILogger then (i.e. the first 3 lines from the answer you link)? – DavidG Aug 07 '17 at 09:52
  • @DavidG: Since there is a `NullLogger` that implements the non-generic `ILogger`, I had hoped there was something similar for generic loggers that I could piggy-back on. – Tomas Aschan Aug 07 '17 at 10:07
  • 2
    Well of course you can change your implementation to take an `ILogger` instead of `ILogger` (the generic version implements the non-generic one anyway) The generic version is really meant to be used for DI anyway. – DavidG Aug 07 '17 at 10:10
  • @DavidG What do you mean with "the generic version is really meant to be used for DI anyway"? DI is at the core of this question - I want to inject one service at runtime, and another at test-time. – Tomas Aschan Aug 07 '17 at 12:49
  • But you are not injecting with DI if you're passing a parameter right? – DavidG Aug 07 '17 at 12:52
  • 1
    @DavidG I'm using DI when I run the entire application, in order to inject something else in my unit tests. This question is about finding the least-resistance way of implementing my tests. – Tomas Aschan Aug 07 '17 at 12:55

10 Answers10

51

Starting from dotnet core 2.0 there's a generic NullLogger<T> class available:

var foo = new Foo(NullLogger<Foo>.Instance);

https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.abstractions.nulllogger-1?view=aspnetcore-2.1 (docs) https://github.com/aspnet/Logging/blob/master/src/Microsoft.Extensions.Logging.Abstractions/NullLoggerOfT.cs (source)

Or if you need it as part of your services:

services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.abstractions.nullloggerfactory?view=aspnetcore-2.1 (docs)

Christoph Lütjen
  • 5,403
  • 2
  • 24
  • 33
16

You can create an instance of ILogger<Foo> using NullLoggerFactory as the factory.

Consider the following controller:

public abstract class Foo: Controller
{
    public Foo(ILogger<Foo> logger) {
        Logger = logger;
    }

    public ILogger Logger { get; private set; }
}

A sample unit test could be:

[TestMethod]
public void FooConstructorUnitTest()
{
    // Arrange
    ILogger<FooController> logger = new Logger<FooController>(new NullLoggerFactory());

    // Act
    FooController target = new FooController(logger);

    // Assert
    Assert.AreSame(logger, target.Logger);
}
Adrian Toman
  • 11,316
  • 5
  • 48
  • 62
10

If you use generic logger (ILogger<>) in your classes those instances are generated from IServiceProvider you should register generic NullLogger<> on service provider as below. Not important what you use generic type T in ILogger<>

services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
3

You have two options:

  1. Create empty implementation of ILogger<Foo> by hand and pass an instance of it to ctor.
  2. Create same empty implementation on the fly using some mocking framework like Moq, NSubstitute, etc.
  • 3
    Since I don't need to verify anything on the logger, providing one with `NSubstitute` turned out to be simple enough. However, it still bugs me that there is a `NullLogger : ILogger`, but not a `NullLogger : ILogger` in the framework... – Tomas Aschan Aug 09 '17 at 13:05
2

You should use the Null Object Pattern. This has two advantages for you: 1) you can get your tests up and running quickly and they won't "blow up", and 2) anyone will be able to use your class without supplying a logger. Just use NullLogger.Instance, or NullLoggerFactory.Instance.

However, you should use a mocking framework to verify that log calls get made. Here is some sample code with Moq.

    [TestMethod]
    public void TestLogError()
    {
        var recordId = new Guid("0b88ae00-7889-414a-aa26-18f206470001");

        _logTest.ProcessWithException(recordId);

        _loggerMock.Verify
        (
            l => l.Log
            (
                //Check the severity level
                LogLevel.Error,
                //This may or may not be relevant to your scenario
                It.IsAny<EventId>(),
                //This is the magical Moq code that exposes internal log processing from the extension methods
                It.Is<It.IsAnyType>((state, t) =>
                    //This confirms that the correct log message was sent to the logger. {OriginalFormat} should match the value passed to the logger
                    //Note: messages should be retrieved from a service that will probably store the strings in a resource file
                    CheckValue(state, LogTest.ErrorMessage, "{OriginalFormat}") &&
                    //This confirms that an argument with a key of "recordId" was sent with the correct value
                    //In Application Insights, this will turn up in Custom Dimensions
                    CheckValue(state, recordId, nameof(recordId))
            ),
            //Confirm the exception type
            It.IsAny<NotImplementedException>(),
            //Accept any valid Func here. The Func is specified by the extension methods
            (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
            //Make sure the message was logged the correct number of times
            Times.Exactly(1)
        );
    }

    private static bool CheckValue(object state, object expectedValue, string key)
    {
        var keyValuePairList = (IReadOnlyList<KeyValuePair<string, object>>)state;

        var actualValue = keyValuePairList.First(kvp => string.Compare(kvp.Key, key, StringComparison.Ordinal) == 0).Value;

        return expectedValue.Equals(actualValue);
    }

For more context, see this article.

Christian Findlay
  • 6,770
  • 5
  • 51
  • 103
2

You could inject ILoggerFactory instead and then create the logger

public Foo(ILoggerFactory loggerFactory) {
    logger = loggerFactory.CreateLogger<Foo>();
    // ...
}

At startup you need to add the NullLoggerFactory service of course:

services.AddSingleton<ILoggerFactory, NullLoggerFactory>()
adospace
  • 1,841
  • 1
  • 17
  • 15
1

From the docs for ILogger<T> (emphasis mine):

A generic interface for logging where the category name is derived from the specified TCategoryName type name. Generally used to enable activation of a named ILogger from dependency injection.

So one option would be to change the implementation of the Foo method to take a plain ILogger and use the NullLogger implementation.

DavidG
  • 113,891
  • 12
  • 217
  • 223
  • 1
    The question was exactly about generic version of `ILogger`. And `Foo` is not just a method, but a constructor, so it is suppose to receive a generic version of `ILogger`. Your advice to replace it with non-generic version is harmful, because it will break the DI as well as correct source context for logged messages in production environment. The answer is absolutely ok for common methods, but not for constructors. – Sane Aug 14 '17 at 15:59
0

If you need to verify the calls in addition to just provide the instance, it gets somewhat complicated. The reason is that most calls does not actually belong to the ILogger interface itself.

I have written a more detailed answer here.

Here is a small overview.

Example of a method that I have made to work with NSubstitute:

public static class LoggerTestingExtensions
{
    public static void LogError(this ILogger logger, string message)
    {
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    }

}

And this is how it can be used:

_logger.Received(1).LogError("Something bad happened");   
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
0

You should try this for mocking ILogger:

mock.Setup(m => m.Log<object>(It.IsAny<LogLevel>(),It.IsAny<EventId>(),It.IsAny<object>(),It.IsAny<Exception>(),It.IsAny<Func<object, Exception,string>>()))
    .Callback<LogLevel, EventId, object, Exception, Func<object, Exception, string>>((logLevel, eventId, obj, exception, func) => 
    {
        string msg = func.Invoke(obj, exception);
        Console.WriteLine(msg);
    });
Bùi Đức Khánh
  • 3,975
  • 6
  • 27
  • 43
0

This worked for me:

private FooController _fooController;
private Mock<ILogger<FooController>> _logger;

[TestInitialize]
public void Setup()
{
    _logger = new Mock<ILogger<FooController>>();
    _fooController = new FooController(_logger.Object);
}
DanielV
  • 2,076
  • 2
  • 40
  • 61