Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to verify ILogger<T>.Log extension method has been called using Moq? [duplicate]

Tags:

c#

xunit

I created a xUnit project to test this sample code

public class ClassToTest
{
    private readonly ILogger<ClassToTest> _logger;

    public ClassToTest(ILogger<ClassToTest> logger)
    {
        _logger = logger;
    }
    
    public void Foo() => _logger.LogError(string.Empty);
}

I installed Moq to create a mock instance for the logger

public class ClassToTestTests
{
    private readonly ClassToTest _classToTest;
    private readonly Mock<ILogger<ClassToTest>> _loggerMock;
    
    public ClassToTestTests()
    {
        _loggerMock = new Mock<ILogger<ClassToTest>>();
        _classToTest = new ClassToTest(_loggerMock.Object);
    }

    [Fact]
    public void TestFoo()
    {
        _classToTest.Foo();
        
        _loggerMock.Verify(logger => logger.LogError(It.IsAny<string>()), Times.Once);
    }
}

When running the tests I get this error message

System.NotSupportedException: Unsupported expression: logger => logger.LogError(It.IsAny(), new[] { })

System.NotSupportedException Unsupported expression: logger => logger.LogError(It.IsAny(), new[] { }) Extension methods (here: LoggerExtensions.LogError) may not be used in setup / verification expressions.

After some research I know that all the log methods are just extension methods. Moq is not able to setup extension methods.

I would like to avoid installing additional third party packages for this problem. Are there any solutions to make the test pass?

like image 296
Question3r Avatar asked Feb 21 '21 21:02

Question3r


People also ask

How do you do a mock ILogger Loginformation?

var log = new List<string>(); var mockLogger = new Mock<ILogger>(); mockLogger. Setup( l => l. Log( It. IsAny<LogLevel>(), It.


1 Answers

You can't mock extension methods.

Instead of mocking

logger.LogError(...)

You need to mock the interface method

void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);

LogError actually calls that interface method like this

logger.Log(LogLevel.Error, 0, new FormattedLogValues(message, args), null, (state, ex) => state.ToString());

So you need to mock

 _loggerMock.Verify(logger => logger.Log(It.Is(LogLevel.Error), It.Is(0), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<TState, Exception, string>>()), Times.Once);

Disclaimer I didn't verify the code

Edit after the comment from pinkfloydx33, I set up a test example in .net50 and came to the following answer

With the most recent framework the FormattedLogValues class has been made internal. So you can't use this with the generic Moq.It members. But Moq has an alternate way to do this (this answer also mentioned the solution)

For a call to the logger like this

_logger.LogError("myMessage");

You need to verify like this

_loggerMock.Verify(logger => logger.Log(
        It.Is<LogLevel>(logLevel => logLevel == LogLevel.Error),
        It.Is<EventId>(eventId => eventId.Id == 0),
        It.Is<It.IsAnyType>((@object, @type) => @object.ToString() == "myMessage" && @type.Name == "FormattedLogValues"),
        It.IsAny<Exception>(),
        It.IsAny<Func<It.IsAnyType, Exception, string>>()),
    Times.Once);

You use the It.IsAnyType for types where you don't have access to. And if you need to restrict the verification you can add a func<object, type> to check if the type is what you expect or cast it to the public interface and validate any public members it has.

When you work with a string message and some parameters you need to cast the object of type FormattedLogValues to interface IReadOnlyList<KeyValuePair<string, object?>> and verify the string/values of the different parameters.

like image 106
verbedr Avatar answered Oct 21 '22 04:10

verbedr