55

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?

Question3r
  • 2,166
  • 19
  • 100
  • 200
  • The extensions themselves call on some other method belonging to the `ILogger` interface. You could check if that method is called instead. – silkfire Feb 21 '21 at 21:53
  • @silkfire except the extension methods use an internal class as the state. So if you want to know/assert what actually was logged, you can't.. And It.IsAny<> won't work since you can't use the internal class for the generic parameter. If you really have such a need I suggest writing a custom ILogger implementation to use in your tests. You can have all messages redirected to a list and then assert their contents directly – pinkfloydx33 Feb 21 '21 at 23:38
  • There are heaps of answers on this e.g., https://stackoverflow.com/a/61741358/2975810 – rgvlee Feb 22 '21 at 09:35
  • Voting to reopen. The dup is about mocking the logger not verifying the logger was called. – ChiefTwoPencils Sep 02 '22 at 20:46

5 Answers5

90

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.

verbedr
  • 1,784
  • 1
  • 15
  • 18
  • 4
    FormattedLogValues is an internal class. This won't work – pinkfloydx33 Feb 21 '21 at 23:40
  • @pinkfloydx33 thanks for pointing out. I guess this has been changed in the more recent framework because I based my first answer on an archived github repo. – verbedr Feb 22 '21 at 09:38
  • `It.IsAnyType` is a new one to me. Cool. That'd been useful to know a while ago – pinkfloydx33 Feb 22 '21 at 11:29
  • 1
    thanks for your help. when replacing my `Verify` call with the last one of yours I get the following exception `Expected invocation on the mock once, but was 0 times: logger => logger.Log(It.Is(logLevel => (int)logLevel == 4), It.Is(eventId => eventId.Id == 0), It.Is((object, type) => object.ToString() == "myMessage" && type.Name == "FormattedLogValues"), It.IsAny(), It.IsAny>())` – Question3r Feb 22 '21 at 20:01
  • I also tried to replace `"myMessage"` with `It.IsAny()` – Question3r Feb 22 '21 at 20:02
  • Hi @Question3r you can just drop the check on the object.ToString() == "myMessage". This was just to point out that you can verify the message that is logged. In your OP it would be string.Empty but it depends on the actual log that is written. The It class can only be used with the verify function. – verbedr Feb 22 '21 at 22:07
  • ah that worked, thanks! but it seems I have to keep `@type.Name == "FormattedLogValues"` right? And this is fix too `eventId.Id == 0` – Question3r Feb 23 '21 at 06:31
  • 3
    It's not needed if you return true it will be fine. The additional checks are only their if this is part of your unit test. E.g. the test requires that that type of message is written. If you only want to know if a message was written but you are not bothered what is written you can just return true like this `(@object, @type) => true` or `eventId => true`. – verbedr Feb 23 '21 at 13:32
  • 7
    @Question3r https://adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moq – Akmal Salikhov Nov 17 '21 at 12:51
  • Despite being in C# development for few years, never knew that calls to extension methods can't be verified. Thank you for your solution, I had issues verifying the call to the main method. – Mocas Sep 16 '22 at 08:07
  • It.Is(logLevel => logLevel == LogLevel.Error) can be replaced with LogLevel.Error – Peheje Dec 28 '22 at 09:31
7

I found the answer Sergio Moreno posted in Git here worked for me:

mock.Verify(
                x => x.Log(
                        It.IsAny<LogLevel>(),
                        It.IsAny<EventId>(),
                        It.Is<It.IsAnyType>((v,t) => true),
                        It.IsAny<Exception>(),
                        It.Is<Func<It.IsAnyType, Exception, string>>((v,t) => true))
John Warlow
  • 2,922
  • 1
  • 34
  • 49
1

The answer by verbedr worked fine, with one change for Net6, I had to make Exception nullable. Here's the setup and verify code for clarity:

_mockLogger.Setup(logger => logger.Log(
    It.Is<LogLevel>(logLevel => logLevel == LogLevel.Error),
    It.IsAny<EventId>(),
    It.Is<It.IsAnyType>((@object, @type) => true),
    It.IsAny<Exception>(),
    It.IsAny<Func<It.IsAnyType, Exception?, string>>()
));

_mockLogger.Verify(
    logger => logger.Log(
        It.Is<LogLevel>(logLevel => logLevel == LogLevel.Error),
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((@object, @type) => true),
        It.IsAny<Exception>(),
        It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
    Times.Once);
balintn
  • 988
  • 1
  • 9
  • 19
1

C# code looks like below using Nuint & Mock framework latest version and its working for me.

Declare:

private Mock<ILogger> _logger;

Inside setup method:

_logger = new Mock<ILogger>();

Inside Test Method:

_logger.Setup(x => x.Log(LogLevel.Information, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>())).Verifiable();
_logger.Verify(logger => logger.Log(
    It.Is<LogLevel>(logLevel => logLevel == LogLevel.Information),
    0,
    It.Is<It.IsAnyType>((@o, @t) => @o.ToString().StartsWith("C# Timer trigger function executed at: ") && @t.Name == "FormattedLogValues"),
    It.IsAny<Exception>(),
    It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.Once);

Inside Teardown method:

_logger.VerifyNoOtherCalls();

-1

I recently used this solution with Moq version 4.18.2. To answer this specific question, I would instead call:

_loggerMock.VerifyLogging(string.Empty, LogLevel.Error, Times.Once());
Benxamin
  • 4,774
  • 3
  • 31
  • 30