Luis Abreu
Luis Abreu

Reputation: 4506

How to mock ILogger.LogXXX methods on netcore 3.0

Until now, we I was mocking the ILogger.LogXXX calls by following this approach.

Unfortunately, after updating the project to .net core 3.0, and if you're using strict mocks (Moq), it will always complain about not having a corresponding setup:

Moq.MockException : ILogger.Log<FormattedLogValues>(LogLevel.Information, 0, 
           Inicio de cancelamento de reserva:  Grm.GestaoFrotas.Dtos.Reservas.Mensagens.MsgCancelamentoReserva, 
           null, 
           Func<FormattedLogValues, Exception, string>) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.

Unfortunately, I can't simple change object with FormattedLogValues like this:

_container.GetMock<ILogger<GestorReservas>>()
          .Setup(l => l.Log(It.IsAny<LogLevel>(),
                            It.IsAny<EventId>(),
                            It.IsAny<FormattedLogValues>(),
                            It.IsAny<Exception>(),
                            It.IsAny<Func<FormattedLogValues, Exception, string>()));

This won't work because FormattedLogValues is internal.

I can always change the mocking strategy (strict to loose), but I'd prefer to keep it as it is (strict). So, any clues on how to solve this?

Thanks.

Upvotes: 6

Views: 2059

Answers (3)

granadaCoder
granadaCoder

Reputation: 27842

This expands on Muk's answer.

My addition is "ILoggerFactory"..and how you "code up" the actually logging inside your real class.

First, unit test code:

using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;


    private Mock<ILoggerFactory> GetDefaultILoggerFactoryMock()
    {
        Mock<ILoggerFactory> returnMock = new Mock<ILoggerFactory>(MockBehavior.Loose);
        returnMock.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(
            () =>
                this.GetDefaultILoggerMock<MyConcreteClassThatUsesILoggerFactoryInItsConstructor>().Object);
        return returnMock;
    }

    private Mock<ILogger<T>> GetDefaultILoggerMock<T>()
    {
        Mock<ILogger<T>> returnMock = new Mock<ILogger<T>>(MockBehavior.Strict);
        returnMock.Setup(
            m => m.Log(
                It.IsAny<LogLevel>(),
                It.IsAny<EventId>(),
                It.IsAny<object>(),
                It.IsAny<Exception>(),
                It.IsAny<Func<object, Exception, string>>())).Callback(
            (LogLevel ll, EventId eid, object obj1, Exception ex, Func<object, Exception, string> func) =>
            {
                Console.WriteLine(func.Invoke(obj1, ex));
            }).Verifiable();
        returnMock.Setup(m => m.IsEnabled(It.IsAny<LogLevel>())).Returns(false);
        return returnMock;
    }

Now, in your actual class (here I call it 'MyConcreteClassThatUsesILoggerFactoryInItsConstructor')..you need to use the NON EXTENSION METHOD of making log calls. (All the helper methods like LogInformation are static extension methods overloads)

So like this: (I am using very "wordy" values to clarify what is what (a log-msg/"state" vs what the formatter does). (Your code probably won't be "wordy" like mine).

        string logMsgAkaTheState = "Here_Is_The_Log_Message_Aka_The_StateArgument";
        Func<object, Exception, string> formatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message)  + "*formatter-end*";
        logger.Log(LogLevel.Information, ushort.MaxValue, logMsgAkaTheState, null, formatterFunc);

or for one that is an exception

        catch (Exception ex)
        {
            string logMsgAkaTheState = "Here_Is_Another_Log_Message_Aka_The_StateArgument";
            Func<object, Exception, string> errorFormatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message)  + "*formatter-end*";

            this.logger.Log(LogLevel.Error, ushort.MaxValue, null, ex, errorFormatterFunc);
        }

If you do both of those things, especially the second part, you can Mock the ILoggerFactory / ILogger

Here is the germane parts of the class/constructor:

using Microsoft.Extensions.Logging;

public class MyConcreteClassThatUsesILoggerFactoryInItsConstructor : IMyConcreteClassThatUsesILoggerFactoryInItsConstructor
{
    public const string ErrorMsgILoggerFactoryIsNull = "ILoggerFactory is null";

    private readonly ILogger<MyConcreteClassThatUsesILoggerFactoryInItsConstructor> logger;
    

    public MyConcreteClassThatUsesILoggerFactoryInItsConstructor(
        ILoggerFactory loggerFactory)
    {
        if (null == loggerFactory)
        {
            throw new ArgumentNullException(ErrorMsgILoggerFactoryIsNull, (Exception)null);
        }

        this.logger = loggerFactory.CreateLogger<MyConcreteClassThatUsesILoggerFactoryInItsConstructor>();

    }

    public void DoSomething()
    {

        string logMsgAkaTheState = "Here_Is_The_Log_Message_Aka_The_StateArgument";
        Func<object, Exception, string> formatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message)  + "*formatter-end*";
        logger.Log(LogLevel.Information, ushort.MaxValue, logMsgAkaTheState, null, formatterFunc);

        int div = 0;

        try
        {
            int x = 1 / div;
        }
        catch (Exception ex)
        {
            string logMsgAkaTheState = "Here_Is_Another_The_Log_Message_Aka_The_StateArgument";
        Func<object, Exception, string> errorFormatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message)  + "*formatter-end*";
        
            this.logger.Log(LogLevel.Error, ushort.MaxValue, null, ex, errorFormatterFunc);
        }


    }

}

Upvotes: -1

Marcel Melzig
Marcel Melzig

Reputation: 363

https://adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moq describes it with a Moq only solution.

loggerMock
  .Verify(l => l.Log(
    It.Is<LogLevel>(l => l == LogLevel.Information),
    It.IsAny<EventId>(),
    It.Is<It.IsAnyType>((v, t) => v.ToString() == "Message"),
    It.IsAny<Exception>(),
    It.IsAny<Func<It.IsAnyType, Exception, string>>()), 
    Times.AtLeastOnce);

Upvotes: -2

Mukmyash
Mukmyash

Reputation: 71

Try this:

//    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
//    <PackageReference Include="Moq" Version="4.13.1" />

container.GetMock<ILogger<GestorReservas>>()
          .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>())));

https://www.bountysource.com/issues/79804378-cannot-verify-calls-to-ilogger-in-net-core-3-0-preview-8-generic-type-matcher-doesn-t-work-with-verify

Upvotes: 6

Related Questions