如何测试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 类型。
升级到 .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);
我想验证一些记录的日志。我正在使用 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 类型。
升级到 .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);