【问题标题】:Cannot test ILogger<T> Received with NSubstitute无法测试使用 NSubstitute 接收的 ILogger<T>
【发布时间】:2020-06-26 07:15:56
【问题描述】:

我有一个 .Net Core 3 应用程序,正在尝试在我的方法中测试对 ILogger 的调用:

public class MyClass
{
    private readonly ILogger<MyClass> _logger;

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

    public void MyMethod(string message)
    {
        _logger.LogError(message);
    }
}

在 SO 和博客上找到答案后,我知道我必须针对接口方法进行测试,而不是扩展方法,所以我有这个测试:

[TestMethod]
public void MyMethodTest()
{
    // Arrange
    var logger = Substitute.For<ILogger<MyClass>>();

    var myClass = new MyClass(logger);

    var message = "a message";

    // Act
    myClass.MyMethod(message);

    // Assert
    logger.Received(1).Log(
        LogLevel.Error,
        Arg.Any<EventId>(),
        Arg.Is<object>(o => o.ToString() == message),
        null,
        Arg.Any<Func<object, Exception, string>>());
}

但是,这不起作用,我收到此错误:

Test method MyLibrary.Tests.MyClassTests.MyMethodTest threw exception: 
NSubstitute.Exceptions.ReceivedCallsException: Expected to receive exactly 1 call matching:
    Log<Object>(Error, any EventId, o => (o.ToString() == value(MyLibrary.Tests.MyClassTests+<>c__DisplayClass0_0).message), <null>, 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.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)
   at MyLibrary.Tests.MyClassTests.MyMethodTest() in D:\Source\Scratch\MyLibrary\MyLibrary.Tests\MyClassTests.cs:line 25

我做错了什么?

netcoreapp3.0 / Microsoft.Extensions.Logging 3.1.2 / NSubstitute 4.2.1

更新:我已经尝试与Arg.Any&lt;&gt;() 匹配并得到相同的结果:

logger.Received(1).Log(
    Arg.Any<LogLevel>(),
    Arg.Any<EventId>(),
    Arg.Any<object>(),
    Arg.Any<Exception>(),
    Arg.Any<Func<object, Exception, string>>());

更新 2:我使用 Moq 尝试了相同的测试并得到相同的结果:

logger.Verify(l => l.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<object>(o => o.ToString() == message),
        null,
        It.IsAny<Func<object, Exception, string>>()),
    Times.Once);

结果:

Test method MyLibrary.Tests.Moq.MyClassTests.MyMethodTest threw exception: 
Moq.MockException: 
Expected invocation on the mock once, but was 0 times: l => l.Log<object>(LogLevel.Error, It.IsAny<EventId>(), It.Is<object>(o => o.ToString() == "a message"), null, It.IsAny<Func<object, Exception, string>>())

Performed invocations:

   Mock<ILogger<MyClass>:1> (l):

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

    at Moq.Mock.Verify(Mock mock, LambdaExpression expression, Times times, String failMessage)
   at Moq.Mock`1.Verify(Expression`1 expression, Times times)
   at Moq.Mock`1.Verify(Expression`1 expression, Func`1 times)
   at MyLibrary.Tests.Moq.MyClassTests.MyMethodTest() in D:\Source\Scratch\MyLibrary\MyLibrary.Tests.Moq\MyClassTests.cs:line 25

【问题讨论】:

  • 您的代码调用LogError(),而您正在验证对Log()的调用...
  • LogError 是一种扩展方法,最终链接到Log - 正如我所说,这是许多博客文章和其他 SO 问题中给出的示例代码
  • @haim770 对此表示感谢...它确实可以说明问题,但我不确定如何继续...在静态扩展上使用某种有效的验证,或将 ILogger 包装在我自己的界面中。 ..两者都不是好的选择。 MELT 解决方案看起来很有趣,但在我的完整实现中,我实际上使用的是 AutoFixture.Freeze,所以不确定它是否合适
  • 我已经选择了“头在沙子里”选项并针对扩展进行了测试。当/如果将来测试中断时,我会担心它

标签: unit-testing .net-core mstest nsubstitute ilogger


【解决方案1】:

使用 .NET Core 3.* 对 ILogger 调用进行单元测试的主要问题是 FormattedLogValues 已更改为内部,这会使事情复杂化。

起订量解决方法是使用It.IsAnyType

public class TestsUsingMoq
{
    [Test]
    public void MyMethod_String_LogsError()
    {
        // Arrange
        var logger = Mock.Of<ILogger<MyClass>>();

        var myClass = new MyClass(logger);

        var message = "a message";

        // Act
        myClass.MyMethod(message);

        //Assert
        Mock.Get(logger)
            .Verify(l => l.Log(LogLevel.Error,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => ((IReadOnlyList<KeyValuePair<string, object>>) o).Last().Value.ToString().Equals(message)),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
                Times.Once);
    }
}

据我所知,NSubstitute 目前没有 It.IsAnyType 等效项,这在尝试使用 Received 方法时会出现问题。但是有一种解决方法,因为它确实提供了一个 ReceivedCalls 方法,您可以对其进行迭代并自己进行调用检查。

public class TestsUsingNSubstitute
{
    [Test]
    public void MyMethod_String_LogsError()
    {
        // Arrange
        var logger = Substitute.For<ILogger<MyClass>>();

        var myClass = new MyClass(logger);

        var message = "a message";

        // Act
        myClass.MyMethod(message);

        //Assert
        Assert.That(logger.ReceivedCalls()
                .Select(call => call.GetArguments())
                .Count(callArguments => ((LogLevel) callArguments[0]).Equals(LogLevel.Error) &&
                                        ((IReadOnlyList<KeyValuePair<string, object>>) callArguments[2]).Last().Value.ToString().Equals(message)),
            Is.EqualTo(1));
    }
}

作为一种解决方法,它不是一个坏的,并且可以很容易地捆绑到一个扩展方法中。

FormattedLogValues 实现IReadOnlyList&lt;KeyValuePair&lt;string, object&gt;&gt;。此列表中的最后一项是您指定的原始消息。

Working sample

【讨论】:

  • 恭喜,它简直太完美了 :-) 对这个主题的关注度很低,这证明了一个可悲的事实,即开发人员很少针对日志规范/期望进行单元测试。否则,过去几年应该会有数百万开发人员登陆这里并支持这个答案。
猜你喜欢
  • 1970-01-01
  • 2018-03-13
  • 2021-01-31
  • 1970-01-01
  • 2017-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多