升级到 .NET Core 3.1 后,Moq 验证未按预期工作

Moq Verify Not Working as Expected After Upgrading to .NET Core 3.1

正如标题所暗示的,这在 .NET Core 2.2 和 Moq 版本 4.10.1 中对我们有用。升级到 .NET Core 3.1 和 Moq 版本 4.14.5 后,验证方法失败,表示尚未调用指定的方法(底层代码没有更改)。我将 Moq 回滚到版本 4.10.1 只是为了看看它是否是由于新版本的 Moq 中的更改所致。我仍然遇到同样的错误。

正在尝试验证日志消息是否已写入 ILogger。

奇怪的是,如果我调试单元测试并用变量监视查看模拟的 object,它表明该方法确实已被调用。

相关代码:

public class AuditFilter, IResultFilter
{
    ...     
    public void OnResultExecuted( ResultExecutedContext context )
    {
        if( !IsContextValid( context ) )
        { return; }
        ...
    }
    
    public override bool IsContextValid( FilterContext context )
    {
        if( context == null )
        { 
            Logger.Error( "Error writing to the audit log", new ArgumentNullException( nameof( context ) ) ); 
            return false;
        }

        if( context.HttpContext == null )
        { 
            Logger.LogError( "Error writing to the audit log", new ArgumentNullException( nameof( context.HttpContext ) ) );
            return false;
        }

        return true;
    }       

    public ILogger Logger { get; set; }
    ...
}

public class AuditTests : BaseTests
{
    ...     
    private Mock<ILogger> _mockLog;
    private AuditFilter _filter;

    [SetUp]
    public void Setup()
    {
        _mockLog = new Mock<Microsoft.Extensions.Logging.ILogger>();
        _mockLog.SetupAllProperties;
        _filter = new AuditFilter();
    }

    [Test]
    public void Service_Filters_Audit_IsContextValid_Context_Null()
    {
        var expected = false;
    
        _filter.Logger = _mockLog.Object;
        var actual = _filter.IsContextValid( null );
    
        _mockLog.Verify( logger => logger.Log( LogLevel.Error, 
            It.IsAny<EventId>(), 
            It.IsAny<object>(),
            It.IsAny<ArgumentNullException>(), 
            It.IsAny<Func<object, Exception, string>>() ),
            Times.Once );
    
        Assert.AreEqual( expected, actual );
    }       
    ...
}

注:方法ILogger.LogError是微软ILogger的扩展方法,调用ILogger.Log方法

下面是抛出的异常。

注意: 整数隐式转换为 Microsoft.Extensions.Logging.EventId,ILogger.Log 方法的第二个输入参数类型。

这些签名似乎与我匹配;所以我不确定为什么它说它没有被调用。

重申一下,此代码在升级到 .NET Core 3.1 之前有效,并且在我们的 pre-forked 代码中仍然有效。

此外,在有人建议需要设置该方法之前:它在没有它的情况下在升级之前工作,我已经尝试过但它没有工作。

我认为问题出在验证表达式的这一部分

It.IsAny<Func<object, Exception, string>>()

在幕后它不是 Func<object, Exception, string> 并且模拟 IsAny 匹配器由于这种差异而不匹配。如果修改为

(Func<object, Exception, string>) It.IsAny<object>()

它应该匹配。以下是我确定的验证表达式的起点,因为它至少适用于 .NET Core 2.* 以上版本:

loggerMock.Verify(x => x.Log(
      It.IsAny<LogLevel>(),
      It.IsAny<EventId>(),
      It.IsAny<IReadOnlyList<KeyValuePair<string, object>>>(),
      It.IsAny<Exception>(),
      (Func<object, Exception, string>) It.IsAny<object>()),
   Times.Exactly(expectedNumberOfInvocations));

您可以使用 It.IsAny<object>() 作为第三个参数,但我发现这在查询属性时更方便。

我最近在工作中为黑盒进程做了一些日志调用验证,所以现在我主要使用 Moq.Contrib.ExpressionBuilders.Logging 来构建流畅、可读的验证表达式。免责声明:我是作者。

就像我在评论中添加的那样,这是一个 known issue 带有 Moq 和 .NET Core 的 .NET Core 3.0 预览版。请查看 github 问题以了解更新失败的原因。

简而言之,从链接的问题来看,这里的失败是由于传递给 Logger.Log 方法的类型现在更改为 FormattedLogValues 造成的。尽管在 Moq 4.13.0 中引入了通用类型匹配器 It.IsAnyType,但这并不能解决问题。

之所以这样,作为Moq的作者之一states here

The important bit (for now) is that It.IsAnyType is not used in a nested position in the It.IsAny<> type argument.

作为解决方法,我们应该更换 It.IsAny<Func<object, Exception, string>>()(Func<object, Exception, string>)It.IsAny<object>()

为此提供的基本原理是为了提高类型匹配器的性能,因为前者会分解引擎盖下的所有类型参数,以查看是否首先包含类型匹配器。