Moq: Extension methods (here: LoggerExtensions.LogError) may not be used in setup / verification expressions.

Sometimes when we develop unit tests it’s convenient to check if the tested code wrote something to the log.

Is it a good practice to assert that some particular message appeared in the logs? I think there are pros and cons. On the pros side, we can make general assertions like “no errors were written to the log”. Testing for error messages can also assure us that a specific edge case was handled. On the con side, it usually feels flaky. This is because log messages tend to change over time. This might require us to update tests every time a log message was improved a bit.

But if we decide to write assertions based on logs, then it’s time to code 🙂 Since the release of .NET Core, the standard logging mechanism is ILogger which comes with the platform. And the most popular library for mocking I encountered in projects is Moq.

When we try to mock ILogger, however, we might find ourselves dealing with an error like:

System.NotSupportedException : Unsupported expression: x => x.LogError(It.IsAny<string>(), new[] {  })
Extension methods (here: LoggerExtensions.LogError) may not be used in setup / verification expressions.
   at Moq.Guard.IsOverridable(MethodInfo method, Expression expression)
   (...)
   at Moq.Mock`1.Verify(Expression`1 expression, Func`1 times)
   at MyProject.UnitTests.When_Something_Expect_Something()
Code language: JavaScript (javascript)

This is because logging implemented in .NET heavily relies on extension methods.

Solution and a working example: before .net 8

Since we cannot mock extension methods, I’d accept the tradeoff where we execute the real extension methods, but mock the object they are operating on. Here’s a working example:

using Microsoft.Extensions.Logging;
using Moq;

namespace MyTestProject;

public class SystemUnderTest
{
    public SystemUnderTest(ILogger logger)
    {
        logger.LogError("Some log message");
    }
}

public class Tests // assuming NUnit is used for unit testing
{
    // Use with most recent version of Moq! With older versions, you might see unexpected behaviors.
    [Test]
    public void When_ObjectIsConstructed_Expect_NoErrorsLogged()
    {
        // Arrange
        var loggerMock = new Mock<ILogger>(MockBehavior.Strict);
        loggerMock.Setup(x => x.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.IsAny<It.IsAnyType>(),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()
            )
        );

        // Act
        var sut = new SystemUnderTest(loggerMock.Object);

        // Assert
        // We can inspect invocations and get logged messages
        var loggedErrorMessages = loggerMock.Invocations
            .Where(x => (LogLevel)x.Arguments[0] == LogLevel.Error)
            .Select(x => x.Arguments[2].ToString());

        // ... or verify that a method was/was not called
        loggerMock.Verify(
            m => m.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.IsAny<It.IsAnyType>(),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Never,
            "Expected no errors would be written to ILogger, but found some"
        );
    }
}

Code language: C# (cs)

Here you can find a minimal C# project with the above code snippet that allows you to see that it compiles and works.

If you want to look for alternative solutions, here’s a good blog post that shows more options on how to unit test ILogger<T>.

Solution and a working example: since .net 8

.NET 8 simplifies things a lot by introducing the FakeLogger class. Here’s a simplistic demo showing how you can use it to test what has been logged in much fewer lines of code than before:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;

namespace MockILoggerWithMoq.Tests;

[TestClass]
public class Tests
{
    [TestMethod]
    public void When_ObjectIsConstructed_Expect_NoErrorsLogged()
    {
        // Arrange
        var logger = new FakeLogger<SystemUnderTest>();

        // Act
        var sut = new SystemUnderTest(logger);

        // Assert
        Assert.IsFalse(logger.Collector.GetSnapshot().Any(e => e.Level == LogLevel.Error),
            "Expected no errors would be written to ILogger, but found some.");
    }
}Code language: JavaScript (javascript)

Have fun!

2 thoughts on “Moq: Extension methods (here: LoggerExtensions.LogError) may not be used in setup / verification expressions.”

  1. Please help me understand why is the last parameter
    It.IsAny<Func>()
    I’m trying to understand how was this derived.
    Thanks!

    Reply
    • Hi Naga M!
      In this example unit test we mock the method `ILogger.Log` which has the following signature:

      “`
      void Log(
      LogLevel logLevel,
      EventId eventId,
      TState state,
      Exception exception,
      Func formatter);
      “`

      Please note the last argument here. The `It.IsAny()` method is used to represent *any* instance of the `Func` formatter delegate. I use `It.IsAny` because I am not concerned here about any specific value of this argument. I want to set up a method method that will work regardless of what formatting options would be passed there.

      I hope this helps a bit!

      Reply

Leave a Comment