【问题标题】:How to verify ILogger<T>.Log extension method has been called using Moq? [duplicate]如何验证 ILogger<T>.Log 扩展方法是否已使用 Moq 调用? [复制]
【发布时间】:2021-05-24 05:02:21
【问题描述】:

我创建了一个 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.LogError(It.IsAny(), new[] { })

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

经过一番研究,我知道所有的日志方法都只是扩展方法。 Moq 无法设置扩展方法。

我想避免为这个问题安装额外的第三方软件包。是否有任何解决方案可以使测试通过?

【问题讨论】:

  • 扩展本身调用属于ILogger接口的其他方法。您可以检查是否调用了该方法。
  • @silkfire 除了扩展方法使用内部类作为状态。因此,如果您想知道/断言实际记录了什么,您就不能......而且 It.IsAny 将不起作用,因为您不能将内部类用于泛型参数。如果您真的有这样的需求,我建议您编写一个自定义 ILogger 实现以在您的测试中使用。您可以将所有消息重定向到一个列表,然后直接声明其内容
  • 对此有很多答案,例如stackoverflow.com/a/61741358/2975810

标签: c# xunit


【解决方案1】:

你不能模拟扩展方法。

而不是嘲笑

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> 并验证不同参数的字符串/值。

【讨论】:

  • FormattedLogValues 是一个内部类。这行不通
  • @pinkfloydx33 感谢您的指出。我想这在最近的框架中已经改变了,因为我的第一个答案是基于一个存档的 github repo。
  • It.IsAnyType 对我来说是一个新人。凉爽的。不久前知道这很有用
  • 感谢您的帮助。当用你的最后一个替换我的Verify 调用时,我得到以下异常Expected invocation on the mock once, but was 0 times: logger =&gt; logger.Log&lt;It.IsAnyType&gt;(It.Is&lt;LogLevel&gt;(logLevel =&gt; (int)logLevel == 4), It.Is&lt;EventId&gt;(eventId =&gt; eventId.Id == 0), It.Is&lt;It.IsAnyType&gt;((object, type) =&gt; object.ToString() == "myMessage" &amp;&amp; type.Name == "FormattedLogValues"), It.IsAny&lt;Exception&gt;(), It.IsAny&lt;Func&lt;It.IsAnyType, Exception, string&gt;&gt;())
  • 不需要返回true就可以了。仅当这是您的单元测试的一部分时,才会进行额外的检查。例如。测试要求写入该类型的消息。如果您只想知道是否写了一条消息,但您不关心所写的内容,您可以像 (@object, @type) =&gt; trueeventId =&gt; true 这样返回 true。
猜你喜欢
  • 1970-01-01
  • 2020-07-03
  • 2016-06-12
  • 2012-02-26
  • 1970-01-01
  • 2013-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多