如何验证 ILogger<T>.Log 扩展方法是否已使用 Moq 调用?

问题描述 投票:0回答:5

我创建了一个 xUnit 项目来测试这个示例代码

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

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

我安装了 Moq 来为记录器创建一个模拟实例

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);
    }
}

运行测试时我收到此错误消息

System.NotSupportedException:不支持的表达式:logger => logger.LogError(It.IsAny(), new[] { })

System.NotSupportedException 不支持的表达式:logger => logger.LogError(It.IsAny(), new[] { }) 扩展方法 (此处:LoggerExtensions.LogError)可能不会在设置中使用/ 验证表达式。

经过一些研究,我知道所有的日志方法都只是扩展方法。最小起订量无法设置扩展方法。

我想避免为这个问题安装额外的第三方包。有什么解决方案可以使测试通过吗?

c# xunit
5个回答
81
投票

你不能模拟扩展方法。

而不是嘲笑

logger.LogError(...)

你需要模拟接口方法

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

LogError 实际上是这样调用那个接口方法

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

所以你需要模拟

 _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);

免责声明我没有验证代码

编辑在pinkfloydx33的评论后,我在.net50中设置了一个测试示例并得出以下答案

使用最新的框架,FormattedLogValues 类已成为内部类。所以你不能将它与通用的 Moq.It 成员一起使用。但是Moq有另一种方法可以做到这一点(这个answer也提到了解决方案)

像这样调用记录器

_logger.LogError("myMessage");

需要这样验证

_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);

您将 It.IsAnyType 用于您无权访问的类型。如果您需要限制验证,您可以添加一个 func 来检查类型是否符合您的预期,或者将其转换为公共接口并验证它拥有的任何公共成员。

当您使用字符串消息和一些参数时,您需要将 FormattedLogValues 类型的对象转换为接口 IReadOnlyList> 并验证不同参数的字符串/值。


5
投票

我找到了 Sergio Moreno 在 Git here 中发布的答案 对我有用:

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))

0
投票

verbedr 的答案工作正常,对 Net6 进行了一次更改,我不得不使 Exception 可以为空。 为了清楚起见,这里是设置和验证代码:

_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);

0
投票

我最近在 Moq 4.18.2 版中使用了这个解决方案。 要回答这个具体问题,我会打电话给:

_loggerMock.VerifyLogging(string.Empty, LogLevel.Error, Times.Once());

0
投票

C# 代码看起来像下面使用 Nuint & Mock 框架最新版本和它为我工作。

声明:

private Mock<ILogger> _logger;

内部设置方法:

_logger = new Mock<ILogger>();

内部测试方法:

_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);

内部拆解方法:

_logger.VerifyNoOtherCalls();

© www.soinside.com 2019 - 2024. All rights reserved.