如何测试asp.net核心内置Ilogger

How to test asp.net core built-in Ilogger

我想验证一些记录的日志。我正在使用 asp.net 核心内置 ILogger,并使用 asp.net 核心内置 DI 注入它:

private readonly ILogger<InvoiceApi> _logger;

public InvoiceApi(ILogger<InvoiceApi> logger)
{
    _logger = logger;
}

然后我会像这样使用它:_logger.LogError("error));

我试着像往常一样模拟它(用最小起订量):

MockLogger = new Mock<ILogger<InvoiceApi>>();

并通过以下方式将其注入服务进行测试:

new InvoiceApi(MockLogger.Object);

然后尝试验证:

MockLogger.Verify(m => m.LogError(It.Is<string>(s => s.Contains("CreateInvoiceFailed"))));

但它抛出:

Invalid verify on a non-virtual (overridable in VB) member: m => m.LogError

那么,我如何验证记录的日志?

LogError 是扩展方法(静态)而不是实例方法。您不能 "directly" 使用模拟框架模拟静态方法(因此扩展方法),因此 Moq 无法模拟并因此验证该方法。我在网上看到了关于 adapting/wrapping 目标接口的建议,并对其进行了模拟,但是如果您在许多地方的代码中使用了默认的 ILogger,那将意味着重写。您必须创建 2 种新类型,一种用于包装器 class,另一种用于可模拟接口。

正如@Nkosi 已经说过的,您不能模拟扩展方法。你应该 嘲笑的是the ILogger.Log method, which LogError calls into。它使验证码有点笨拙,但它应该可以工作:

MockLogger.Verify(
    m => m.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<FormattedLogValues>(v => v.ToString().Contains("CreateInvoiceFailed")),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()
    )
);

(不确定这是否编译,但你明白了要点)

我写了一篇short article showing a variety of approaches, including mocking the underlying Log() method as described in other answers here. The article includes a full GitHub repo with each of the different options。最后,如果您需要能够测试它是否被调用,我的建议是使用您自己的适配器而不是直接使用 ILogger 类型。

https://ardalis.com/testing-logging-in-aspnet-core

升级到 .net core 3.1 后,FormattedLogValues 成为内部版本。我们不能再访问它了。我做了一些更改的扩展方法。 扩展方法的一些示例用法:

mockLogger.VerifyLog(Times.Once);
public static void VerifyLog<T>(this Mock<ILogger<T>> mockLogger, Func<Times> times)
{
    mockLogger.Verify(x => x.Log(
        It.IsAny<LogLevel>(),
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((v, t) => true),
        It.IsAny<Exception>(),
        It.Is<Func<It.IsAnyType, Exception, string>>((v, t) => true)), times);
}

对于那些试图接收日志调用回调的人:

            mock.Mock<ILogger<X>>()
                .Setup(x =>
                    x.Log(
                        LogLevel.Information,
                        It.IsAny<EventId>(),
                        It.IsAny<It.IsAnyType>(),
                        It.IsAny<Exception>(),
                        (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()
                    )
                )
                .Callback<LogLevel, EventId, object, Exception, Delegate>(
                    (level, eventid, state, ex, func) =>
                    {
                        this.Out.WriteLine(state.ToString());
                    }
                );

我必须对工作中的黑盒进程进行相当多的记录器验证,在忍受了短时间的表达式后,我屈服并组装了一个表达式构建器 - Moq.Contrib.ExpressionBuilders.Logging

最终结果是更短、更流畅的验证语句:

logger.Verify(
  Log.With.LogLevel(LogLevel.Error)
    .And.EventId(666)
    .And.LogMessage("I am a meat popsicle"), 
  Times.Once);

有一个 github issue 向您展示了如何验证记录器的使用情况。

假设我们有一个要模拟的 ILogger<MyService>

private readonly Mock<ILogger<MyService>> loggerMock = new Mock<ILogger<MyService>>();

验证零调用

loggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), 
  It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), 
  (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), 
  Times.Never);

验证日志级别和次数

const LogLevel expectedLevel = LogLevel.Debug;
Times expectedOccasions = Times.Exactly(1);

loggerMock.Verify(l => l.Log(expectedLevel, It.IsAny<EventId>(), 
  It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), 
  (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),  
  expectedOccasions);

验证日志消息

const LogLevel expectedLevel = LogLevel.Information;
const string expectedMessage = "Information";
const Exception withoutException = null;
Times expectedOccasions = Times.Once;

loggerMock.Verify(logger => logger.Log(expectedLevel, It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(), withoutException, 
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), 
    expectedOccasions, expectedMessage);

const LogLevel expectedLevel = LogLevel.Information;
const string expectedMessage = "Information";
const Exception withoutException = null;
Times expectedOccasions = Times.Once;

loggerMock.Verify(logger => logger.Log(expectedLevel, It.IsAny<EventId>(),
    It.Is<It.IsAnyType>((v, _) => v.ToString().Contains(expectedMessage)),
    withoutException, (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), 
    expectedOccasions);

验证异常类型

const LogLevel expectedLevel = LogLevel.Critical;
Times expectedOccasions = Times.Once;
                
loggerMock.Verify(l => l.Log(expectedLevel , It.IsAny<EventId>(),  
   It.IsAny<It.IsAnyType>(), It.IsAny<MyCustomException>(), 
   (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), 
   expectedOccasions);