如何检查 ASP.NET Core 3.0 的单元测试中是否记录了错误?

How to check that an error is logged in a unit test for ASP.NET Core 3.0?

我想创建一个单元测试以确保方法使用 xUnit 和 Moq 记录错误。此代码适用于 ASP.NET Core 2.1:

//Arrange
var logger = new Mock<ILogger<MyMiddleware>>();
var httpContext = new DefaultHttpContext();

var middleware = new MyMiddleware(request => Task.FromResult(httpContext), logger.Object);

//Act
await middleware.InvokeAsync(httpContext);

//Assert
logger.Verify(x => x.Log(LogLevel.Error, It.IsAny<EventId>(), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()), Times.Once);

验证 _logger.LogError("Error message");middleware.InvokeAsync 中被调用。

但是,在 ASP.NET Core 3.0 中,我无法验证是否正在调用记录器。 Microsoft.Extensions.Logging.Internal 无法再引用,因此 FormattedLogValues 不可用。

我尝试将 Assert() 更改为使用 object 而不是 FormattedLogValues,还有 IReadOnlyList<KeyValuePair<string, object>> 因为那是 FormattedLogValues 的基础(FormattedLogValues.cs).

这是我在 Visual Studio 测试运行程序中收到的错误消息:

Message: 
    Moq.MockException : 
    Expected invocation on the mock once, but was 0 times: x => x.Log<object>(LogLevel.Error, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>())

  Performed invocations:

    ILogger.Log<FormattedLogValues>(LogLevel.Error, 0, Error message, null, Func<FormattedLogValues, Exception, string>)

  Stack Trace: 
    Mock.Verify(Mock mock, LambdaExpression expression, Times times, String failMessage)
    Mock`1.Verify(Expression`1 expression, Times times)
    Mock`1.Verify(Expression`1 expression, Func`1 times)
    MyMiddlewareTests.InvokeAsync_ErrorIsLogged() line 35
    --- End of stack trace from previous location where exception was thrown ---

如何验证 ASP.NET Core 3.0 中记录的错误?

aspnet github 页面上有关于此的 an issue。看来问题出在 Moq 上,他们已经做出更改来修复它。

您需要升级到 Moq 4.13 才能获得修复。

他们引入了 It.IsAnyType 来解决内部对象的问题,因此您应该能够将 object 引用更改为 It.IsAnyType 并像这样编写测试:

logger.Verify(x => x.Log(LogLevel.Error, It.IsAny<EventId>(), It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), Times.Once);

注意:最后一个参数需要进行类型转换,因为 Moq 目前不支持嵌套类型匹配器。

可以从 Moq 中找到更多详细信息 here

我使用 SGA(设置、抓取、断言)方法。

//In the setup part
var mockLog = new Mock<ILogger>(MockBehavior.Strict);
string error = null;
mockLog.Setup(l => l
    .Error(It.IsAny<Exception>(), It.IsAny<string>()))
    .Callback((Exception b, string c) =>
    {
        error = c + " " + b.GetBaseException().Message;
        // This would keep only the last error, but it's OK, since there should be zero
        // Sometimes I use a collection and append the error info.
    });


//In the assert part
Assert.IsNull(error, $"Error detected: {error}");

这有点晚了,但我只想补充一点,如果您断言要记录特定的消息,您可以创建一个自定义匹配器来检查消息:

 logger.Verify(x => x.Log(LogLevel.Error,
      It.IsAny<EventId>(),
      It.Is<It.IsAnyType>((x, _) => LogMessageMatcher(x, "Expected error message")),
     It.IsAny<Exception>(),
     It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once);

MatcherMethod 将是一个静态方法,如下所示:

     public static bool LogMessageMatcher(object formattedLogValueObject, string message)
    {
        var logValues = formattedLogValueObject as IReadOnlyList<KeyValuePair<string, object>>;
        return logValues.FirstOrDefault(logValue => logValue.Key == "{OriginalFormat}")
                         .Value.ToString() == message;
    }