Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you mock ILogger LogInformation

I have a class that receives an ILogger and I want to mock the LogInformation calls but this is an extension method. How do I make the appropiate setup call for this?

like image 994
orellabac Avatar asked Oct 08 '18 17:10

orellabac


People also ask

How do you do a mock ILogger?

To mock an ILogger<T> object, we can use Moq library to instantiate a new Mock<ILogger<T>>() object. Make sure you have installed the latest Moq package (v4. 15.1 as of Nov. 16, 2020, which contains an update to support “nested” type matchers).


4 Answers

If you're using Moq >= 4.13, here is a way to mock ILogger:

logger.Verify(x => x.Log(
    It.IsAny<LogLevel>(),
    It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(),
    It.IsAny<Exception>(),
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

You can change the It.IsAny<LogLevel>(), It.IsAny<EventId>(), and It.IsAny<Exception>() stubs to be more specific, but using It.IsAnyType is necessary because FormattedLogValues is now internal.

Reference: TState in ILogger.Log used to be object, now FormattedLogValues

like image 79
crgolden Avatar answered Nov 02 '22 01:11

crgolden


Example with a Callback, tested with Moq 4.14.5. More Informations available on this Github Issue

PM> install-package Moq

var logger = new Mock<ILogger<ReplaceWithYourObject>>();

logger.Setup(x => x.Log(
    It.IsAny<LogLevel>(),
    It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(),
    It.IsAny<Exception>(),
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
    .Callback(new InvocationAction(invocation =>
    {
        var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
        var eventId = (EventId)invocation.Arguments[1];  // so I'm not sure you would ever want to actually use them
        var state = invocation.Arguments[2];
        var exception = (Exception)invocation.Arguments[3];
        var formatter = invocation.Arguments[4];

        var invokeMethod = formatter.GetType().GetMethod("Invoke");
        var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });
    }));

A complete generic helper class for UnitTesting

public static class LoggerHelper
{
    public static Mock<ILogger<T>> GetLogger<T>()
    {
        var logger = new Mock<ILogger<T>>();

        logger.Setup(x => x.Log(
            It.IsAny<LogLevel>(),
            It.IsAny<EventId>(),
            It.IsAny<It.IsAnyType>(),
            It.IsAny<Exception>(),
            (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
            .Callback(new InvocationAction(invocation =>
            {
                var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
                var eventId = (EventId)invocation.Arguments[1];  // so I'm not sure you would ever want to actually use them
                var state = invocation.Arguments[2];
                var exception = (Exception)invocation.Arguments[3];
                var formatter = invocation.Arguments[4];

                var invokeMethod = formatter.GetType().GetMethod("Invoke");
                var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });

                Trace.WriteLine($"{logLevel} - {logMessage}");
            }));

        return logger;
    }
}
like image 22
live2 Avatar answered Nov 02 '22 01:11

live2


ILogger is normally used thru extension methods, LogWarning, LogError, etc.

In my case I was interested in the LogWarning method which after looking at the code calls the Log method from ILogger. In order to mock it with Moq, this is what I ended up doing:

     var list = new List<string>();
                var logger = new Mock<ILogger>();
                logger
                    .Setup(l => l.Log<FormattedLogValues>(LogLevel.Warning, It.IsAny<EventId>(), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<FormattedLogValues, Exception, string>>()))
                    .Callback(
                    delegate (LogLevel logLevel, EventId eventId, FormattedLogValues state, Exception exception, Func<FormattedLogValues, Exception, string> formatter)
                    {
                        list.Add(state.ToString());
                    });

In newer versions of .NET Core 3.0 this won't work. Because FormattedLogValues is an internal type. You need to update the moq version to at least:

`<PackageReference Include="Moq" Version="4.16.0" />`

After updating Moq The workaround is like this:

            var log = new List<string>();
            var mockLogger = new Mock<ILogger>();
            mockLogger.Setup(
                l => l.Log(
                    It.IsAny<LogLevel>(),
                    It.IsAny<EventId>(),
                    It.IsAny<It.IsAnyType>(),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
                .Callback((IInvocation invocation) =>
                {
                    var logLevel = (LogLevel)invocation.Arguments[0];
                    var eventId = (EventId)invocation.Arguments[1];
                    var state = (IReadOnlyCollection<KeyValuePair<string, object>>)invocation.Arguments[2];
                    var exception = invocation.Arguments[3] as Exception;
                    var formatter = invocation.Arguments[4] as Delegate;
                    var formatterStr = formatter.DynamicInvoke(state, exception);
                    log.Add(
                      $"{logLevel} - {eventId.Id} - Testing - {formatterStr}");
                });

Notice the special cast: (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()) and also the IInvocation to handle the arguments.

like image 22
orellabac Avatar answered Nov 02 '22 03:11

orellabac


This is how I workaround for Moq (v4.10.1) framework.

public static class TestHelper
{ 

    public static Mock<ILogger<T>> GetMockedLoggerWithAutoSetup<T>()
    {
        var logger = new Mock<ILogger<T>>();

        logger.Setup<object>(x => x.Log(
       It.IsAny<LogLevel>(),
       It.IsAny<EventId>(),
       It.IsAny<object>(),
       It.IsAny<Exception>(),
       It.IsAny<Func<object, Exception, string>>()));

        return logger;
    }

    public static void VerifyLogMessage<T>(Mock<ILogger<T>> mockedLogger, LogLevel logLevel, Func<string, bool> predicate, Func<Times> times)
    {
        mockedLogger.Verify(x => x.Log(logLevel, 0, It.Is<object>(p => predicate(p.ToString())), null, It.IsAny<Func<object, Exception, string>>()), times);
    }
}

--

public class Dummy
{

}

[Fact]
public void Should_Mock_Logger()
{
    var logger = TestHelper.GetMockedLoggerWithAutoSetup<Dummy>();
    logger.Object.LogInformation("test");
    TestHelper.VerifyLogMessage<Dummy>(logger, LogLevel.Information, msg => msg == "test", Times.Once);
}

--

The thing is,

If I had chosen any other <TCustom> than <object> for logger.Setup(), it would fail on Verify step saying that 0 calls were made for x.Log<TCustom> and showing a call made to x.Log<object>. So I setup my generic logger to mock Log<object>(..) method instead.

like image 24
Cankut Avatar answered Nov 02 '22 02:11

Cankut