Moq Verify non funziona come previsto dopo l'aggiornamento a .NET Core 3.1

Aug 22 2020

Quindi, come suggerisce il titolo, questo ha funzionato per noi in .NET Core 2.2 e Moq versione 4.10.1. Dopo l'aggiornamento a .NET Core 3.1 e Moq versione 4.14.5, il metodo di verifica ha esito negativo e indica che il metodo specificato non è stato chiamato (nessuna modifica al codice sottostante). Ho ripristinato Moq alla versione 4.10.1 solo per vedere se fosse dovuto a un cambiamento all'interno della nuova versione di Moq. Ottengo ancora lo stesso errore.

Tentativo di verificare che un messaggio di log sia stato scritto in ILogger.

La cosa strana è che, se eseguo il debug del test unitario e guardo l'oggetto deriso con un orologio variabile, mostra che il metodo è stato effettivamente invocato.

Codice rilevante:

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

Nota: il metodo ILogger.LogError è un metodo di estensione Microsoft per ILogger che chiama il metodo ILogger.Log.

Di seguito è riportata l'eccezione generata.

Nota: un numero intero esegue implicitamente il cast a Microsoft.Extensions.Logging.EventId, il secondo tipo di parametro di input per il metodo ILogger.Log.

Queste firme sembrano corrispondere a me; quindi non sono sicuro del motivo per cui sta dicendo che non è stato invocato.

Per ribadire, questo codice ha funzionato prima dell'aggiornamento a .NET Core 3.1 e funziona ancora nel nostro codice pre-fork.

Inoltre, prima che qualcuno suggerisca che il metodo deve essere configurato: ha funzionato prima dell'aggiornamento senza di esso, e l'ho già provato e non ha funzionato.

Risposte

SaiGummaluri Aug 22 2020 at 13:39

Come ho aggiunto nei commenti, questo è un problema noto con Moq e .NET Core fin dalla versione di anteprima di .NET Core 3.0. Dai un'occhiata al problema di github per capire perché questo si è rotto con l'aggiornamento.

In poche parole, dal problema collegato, l'errore qui è causato dal fatto che il tipo passato al Logger.Logmetodo è ora cambiato in FormattedLogValues. Sebbene sia stato introdotto un matcher di tipo generico It.IsAnyTypein Moq 4.13.0, ciò non risolverebbe il problema.

La ragione di ciò, come afferma qui uno degli autori di Moq

Il bit importante (per ora) è che It.IsAnyTypenon viene utilizzato in una posizione annidata It.IsAny<>nell'argomento di tipo.

Come soluzione alternativa dovremmo sostituire It.IsAny<Func<object, Exception, string>>()con(Func<object, Exception, string>)It.IsAny<object>()

La logica fornita per questo è migliorare le prestazioni del type matcher poiché il primo comporterebbe la scomposizione di tutti i parametri di tipo sotto il cofano per vedere se c'è un type matcher incluso in primo luogo.

1 rgvlee Aug 22 2020 at 08:02

Credo che il problema sarà questa parte dell'espressione di verifica

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

Sotto le coperte non è un Func<object, Exception, string>e il finto matcher IsAny non corrisponde a causa di questa differenza. Se lo modifichi in

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

dovrebbe corrispondere. Di seguito è riportato ciò che ho scelto come punto di partenza per verificare le espressioni in quanto funziona almeno per .NET Core 2.* in poi:

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

Puoi usare It.IsAny<object>()per il terzo parametro ma trovo che sia più conveniente quando si interrogano le proprietà.

Di recente ho eseguito un bel po 'di verifica delle chiamate di registro al lavoro per i processi della scatola nera, quindi ora utilizzo principalmente Moq.Contrib.ExpressionBuilders.Logging per creare espressioni di verifica fluide e leggibili. Disclaimer: sono l'autore.