如何正确检查 ILogger.LogCritical 是否已被 NSubstitute 调用?

How to check properly that ILogger.LogCritical has been called with NSubstitute?

我在使用 NSubstitute 检查方法 ILogger.LogCritical(...) 是否被调用时遇到了一些困难。

例如下面的代码:

[Fact]
public void TestNSubstituteAgain()
{
    var logger = Substitute.For<ILogger<StockService>>();
    logger.LogCritical(new Exception(), "Hey lads!");
    logger.Received().Log(
        LogLevel.Critical, 
        Arg.Any<EventId>(), 
        Arg.Any<object>(), 
        Arg.Any<Exception>(),
        Arg.Any<Func<object, Exception, string>>()
        );
}

我遇到了异常:

Rm.Combo.App.Tests.VirtualParcels.Services.StockServiceTest.TestNSubstituteAgain

NSubstitute.Exceptions.ReceivedCallsException : Expected to receive a call matching:
    Log<Object>(Critical, any EventId, any Object, any Exception, any Func<Object, Exception, String>)
Actually received no matching calls.

   at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity)
   at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call)
   at NSubstitute.Routing.Route.Handle(ICall call)
   at NSubstitute.Core.CallRouter.Route(ICall call)
   at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.Proxies.ObjectProxy_2.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)
   at Rm.Combo.App.Tests.VirtualParcels.Services.StockServiceTest.TestNSubstituteAgain() in C:\Users\eperret\Desktop\combo\api\Rm.Combo.App.Tests\VirtualParcels\Services\StockServiceTest.cs:line 99
// 1] Ok the call I am making is that ext. method below:
public static void LogCritical(this ILogger logger, Exception exception, string message, params object[] args)
{
    logger.Log(LogLevel.Critical, exception, message, args);
}

// 2] Which forwards to that other ext. method below:
public static void Log(this ILogger logger, LogLevel logLevel, Exception exception, string message, params object[] args)
{
    logger.Log(logLevel, 0, exception, message, args);
}

// 3] and... then here (still an ext. method tho):
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, string message, params object[] args)
{
    if (logger == null)
    {
        throw new ArgumentNullException(nameof(logger));
    }

    logger.Log(logLevel, eventId, new FormattedLogValues(message, args), exception, _messageFormatter);
}

// 4] to finally end up with this "direct" interface method:
void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter);
// Note: which is supposedly what I am gonna ask NSubstitute to check against, right? (since we cannot use NSubstitute `Received` method on ext. method, it has to be the corresponding instance method)

注意:FormattedLogValues 是一个 internal struct 所以我真的不得不使用 Arg.Any<object>

当我检查实际收到的呼叫时,我看到记录器有一个呼叫,但我不太确定它与我所断言的有何不同。就像我在调试单元测试时在 var re = logger.ReceivedCalls(); 之类的东西上设置断点:

我真的不知道什么不被视为 NSubstitute Received() 断言的正确参数。

有什么想法吗?

如果参数匹配非常困难,我会使用 ReceivedWithAnyArgs() (https://nsubstitute.github.io/help/received-calls/)。如果事情很简单,NSubstitute 很好。如果它变得太复杂,我倾向于只创建接口的测试版本(例如 ILogger),并从那里的调用中注册我感兴趣的参数,并检查那些。

我最终使用 Moq 进行了特定类型的测试:

// Defining an extension method
public static void IsLogReceived(this IMock<ILogger<StockService>>logger, LogLevel level, int count = 1) =>
    logger.Verify(x =>
        x.Log(level,
            It.IsAny<EventId>(),
            It.IsAny<It.IsAnyType>(),
            It.IsAny<Exception>(),
            (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), 
        Times.Exactly(count));

// [...]

// and using the ext. method like that:
[Fact]
public void TestNSubstituteAgain()
{
    var loggerMock = new Mock<ILogger<StockService>>();
    var logger = loggerMock.Object;
    logger.LogCritical(new Exception(), "Hey lads!");
    loggerMock.IsLogReceived(LogLevel.Critical);
}

它不是那么准确,但取决于方法范围内发生的事情,可以完成工作。

话虽这么说,但我承认最准确的实现方式是声明一个包装类型。

对我有点帮助的资源: