.NET Core 3.1にアップグレードした後、Moq検証が期待どおりに機能しない

Aug 22 2020

タイトルが示すように、これは.NET Core2.2およびMoqバージョン4.10.1で機能しました。.NET Core 3.1およびMoqバージョン4.14.5にアップグレードした後、verifyメソッドが失敗し、指定されたメソッドが呼び出されていない(基になるコードに変更がない)と表示されます。新しいバージョンのMoq内での変更によるものかどうかを確認するために、Moqをバージョン4.10.1にロールバックしました。それでも同じエラーが発生します。

ログメッセージがILoggerに書き込まれたことを確認しようとしています。

奇妙なことに、単体テストをデバッグし、変数watchを使用してモックオブジェクトを見ると、メソッドが実際に呼び出されていることがわかります。

関連コード:

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.Logメソッドを呼び出すILoggerのMicrosoft拡張メソッドです。

以下は、スローされる例外です。

注:整数は、ILogger.Logメソッドの2番目の入力パラメーター型であるMicrosoft.Extensions.Logging.EventIdに暗黙的にキャストされます。

これらの署名は私と一致しているようです。だから、なぜそれが呼び出されなかったと言っているのかわかりません。

繰り返しになりますが、このコードは.NET Core 3.1にアップグレードする前に機能し、事前にフォークされたコードでも機能します。

また、誰かがメソッドをセットアップする必要があると提案する前に、それなしでアップグレードする前に機能しましたが、私はすでにそれを試しましたが、機能しませんでした。

回答

SaiGummaluri Aug 22 2020 at 13:39

コメントで追加したように、これは.NET Core3.0のプレビューバージョンからのMoqおよび.NETCoreの既知の問題です。githubの問題を見て、これがアップデートで壊れた理由を理解してください。

一言で言えば、リンクされた問題から、ここでの失敗は、Logger.Logメソッドに渡される型がに変更されたという事実が原因で発生しFormattedLogValuesます。It.IsAnyTypeMoq 4.13.0で導入されたジェネリック型マッチャーがありますが、それでは問題に対処できません。

この理由は、Moqの作者の1人がここで述べているように

(今のところ)重要なビットIt.IsAnyTypeは、It.IsAny<>type引数のネストされた位置では使用されないことです。

回避策として、次のように置き換える必要がIt.IsAny<Func<object, Exception, string>>()あります(Func<object, Exception, string>)It.IsAny<object>()

これに提供される理論的根拠は、タイプマッチャーのパフォーマンスを向上させることです。前者は、フードの下にあるすべてのタイプパラメーターを分解して、最初にタイプマッチャーが含まれているかどうかを確認するためです。

1 rgvlee Aug 22 2020 at 08:02

問題は検証式のこの部分になると思います

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>()3番目のパラメータに使用できますが、プロパティを調べるときにこれが便利だと思います。

私は最近、ブラックボックスプロセスに対してかなりのログ呼び出し検証を行っているので、今では主にMoq.Contrib.ExpressionBuilders.Loggingを使用して流暢で読みやすい検証式を構築しています。免責事項:私は著者です。