你如何模拟 ILogger LogInformation

ore*_*bac 27 c# logging mocking

我有一个接收 ILogger 的类,我想模拟 LogInformation 调用,但这是一个扩展方法。我如何为此进行适当的设置调用?

crg*_*den 27

如果您使用的是 Moq >= 4.13,这里是一种模拟方法ILogger

logger.Verify(x => x.Log(
    It.IsAny<LogLevel>(),
    It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(),
    It.IsAny<Exception>(),
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
Run Code Online (Sandbox Code Playgroud)

你可以改变的It.IsAny<LogLevel>()It.IsAny<EventId>()It.IsAny<Exception>()存根更具体,但使用It.IsAnyType是必要的,因为FormattedLogValues现在internal

参考:ILogger.Log 中的 TState 以前是对象,现在是 FormattedLogValues

  • 我找到了这个解决方案:https://adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moq。它描述了一种仅起订量的解决方案,同时也验证消息。`loggerMock .Verify(l =&gt; l.Log( It.Is&lt;LogLevel&gt;(l =&gt; l == LogLevel.Information), It.IsAny&lt;EventId&gt;(), It.Is&lt;It.IsAnyType&gt;((v , t) =&gt; v.ToString() == "Message"), It.IsAny&lt;Exception&gt;(), It.IsAny&lt;Func&lt;It.IsAnyType, Exception, string&gt;&gt;()), Times.AtLeastOnce); ` (4认同)
  • 我的王国,如果你也显示设置的回调! (3认同)

liv*_*ve2 16

带有回调的示例,使用 Moq 4.14.5 进行测试。有关此Github 问题的更多信息

var logger = new Mock<ILogger<ReplaceWithYourObject>>();

logger.Setup(x => x.Log(
    It.IsAny<LogLevel>(),
    It.IsAny<EventId>(),
    It.IsAny<It.IsAnyType>(),
    It.IsAny<Exception>(),
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
    .Callback(new InvocationAction(invocation =>
    {
        var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
        var eventId = (EventId)invocation.Arguments[1];  // so I'm not sure you would ever want to actually use them
        var state = invocation.Arguments[2];
        var exception = (Exception?)invocation.Arguments[3];
        var formatter = invocation.Arguments[4];

        var invokeMethod = formatter.GetType().GetMethod("Invoke");
        var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });
    }));
Run Code Online (Sandbox Code Playgroud)

一个完整的 UnitTesting 通用辅助类

public static class LoggerHelper
{
    public static Mock<ILogger<T>> GetLogger<T>()
    {
        var logger = new Mock<ILogger<T>>();

        logger.Setup(x => x.Log(
            It.IsAny<LogLevel>(),
            It.IsAny<EventId>(),
            It.IsAny<It.IsAnyType>(),
            It.IsAny<Exception>(),
            (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
            .Callback(new InvocationAction(invocation =>
            {
                var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above
                var eventId = (EventId)invocation.Arguments[1];  // so I'm not sure you would ever want to actually use them
                var state = invocation.Arguments[2];
                var exception = (Exception?)invocation.Arguments[3];
                var formatter = invocation.Arguments[4];

                var invokeMethod = formatter.GetType().GetMethod("Invoke");
                var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception });

                Trace.WriteLine(logMessage);
            }));

        return logger;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 很棒的帖子 - https://adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moq (2认同)

Sco*_*nen 12

在这种情况下,“假”课程可能比起订量更容易。创建起来需要多一点工作,但是您可以永远重复使用它,并且它比 Moq 回调更容易阅读和使用。(我喜欢 Moq,但当有更简单的方法时就不喜欢。)

对于大多数用例,这将按原样工作,或者您可以对其进行调整。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;

public class FakeLogger<T> : ILogger, ILogger<T>
{
    public List<LogEntry> LogEntries { get; } = new List<LogEntry>();

    public IEnumerable<LogEntry> InformationEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Information);

    public IEnumerable<LogEntry> WarningEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Warning);

    public IEnumerable<LogEntry> ErrorEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Error);

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        LogEntries.Add(new LogEntry(logLevel, eventId, state, exception));
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return new LoggingScope();
    }

    public class LoggingScope : IDisposable
    {
        public void Dispose()
        {
        }
    }
}

public class LogEntry
{
    public LogEntry(LogLevel logLevel, EventId eventId, object state, Exception exception)
    {
        LogLevel = logLevel;
        EventId = eventId;
        State = state;
        Exception = exception;
    }

    public LogLevel LogLevel { get; }
    public EventId EventId { get; }
    public object State { get; }
    public Exception Exception { get; }
    public string Message => State?.ToString() ?? string.Empty;
    public Dictionary<string, object> Params => (State as IReadOnlyList<KeyValuePair<string, object?>>)!.ToDictionary(key => key.Key, value => value.Value)!;
    public string FormatString => (string)Params["{OriginalFormat}"];
}
Run Code Online (Sandbox Code Playgroud)

创建一个实例并将其作为记录器注入到您的测试类中。然后您可以查看LogEntries集合中的对象以了解记录的内容。

的类型State通常为FormattedLogValues,但您可以调用State.ToString()并仅获取字符串值。

  • 最简单的方法,老实说。尝试了此处列出的其他一些,但遇到了问题。人们很容易忘记,有时假货比模仿品更好。聪明的思维。 (3认同)
  • 这是我最终寻找自己的答案并一遍又一遍地粘贴代码的少数情况之一。 (2认同)

Ank*_*kar 9

如果你有一个logger( new Mock<ILogger<ReplaceWithYourObject>>()) 模拟注入到 ctor 中。那么这应该有助于验证日志消息和日志级别。

logger.Verify(x => x.Log(
       LogLevel.Information, //Change LogLevel as required
       It.IsAny<EventId>(),
       It.Is<It.IsAnyType>((object v, Type _) => 
       v.ToString().Contains("MessageToVerify")), // Change MessageToVerify as required
       It.IsAny<Exception>(),
       (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
Run Code Online (Sandbox Code Playgroud)


ore*_*bac 6

ILogger 通常通过扩展方法、LogWarning、LogError 等使用。

就我而言,我对 LogWarning 方法感兴趣,在查看代码后,该方法从 ILogger 调用 Log 方法。为了用 Moq 模拟它,这就是我最终做的:

     var list = new List<string>();
                var logger = new Mock<ILogger>();
                logger
                    .Setup(l => l.Log<FormattedLogValues>(LogLevel.Warning, It.IsAny<EventId>(), It.IsAny<FormattedLogValues>(), It.IsAny<Exception>(), It.IsAny<Func<FormattedLogValues, Exception, string>>()))
                    .Callback(
                    delegate (LogLevel logLevel, EventId eventId, FormattedLogValues state, Exception exception, Func<FormattedLogValues, Exception, string> formatter)
                    {
                        list.Add(state.ToString());
                    });
Run Code Online (Sandbox Code Playgroud)

在较新版本的 .NET Core 3.0 中,这不起作用。因为 FormattedLogValues 是内部类型。您需要将最小起订量版本更新为至少:

`<PackageReference Include="Moq" Version="4.16.0" />`
Run Code Online (Sandbox Code Playgroud)

更新后Moq解决方法是这样的:

            var log = new List<string>();
            var mockLogger = new Mock<ILogger>();
            mockLogger.Setup(
                l => l.Log(
                    It.IsAny<LogLevel>(),
                    It.IsAny<EventId>(),
                    It.IsAny<It.IsAnyType>(),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()))
                .Callback((IInvocation invocation) =>
                {
                    var logLevel = (LogLevel)invocation.Arguments[0];
                    var eventId = (EventId)invocation.Arguments[1];
                    var state = (IReadOnlyCollection<KeyValuePair<string, object>>)invocation.Arguments[2];
                    var exception = invocation.Arguments[3] as Exception;
                    var formatter = invocation.Arguments[4] as Delegate;
                    var formatterStr = formatter.DynamicInvoke(state, exception);
                    log.Add(
                      $"{logLevel} - {eventId.Id} - Testing - {formatterStr}");
                });
Run Code Online (Sandbox Code Playgroud)

注意特殊的 cast:(Func<It.IsAnyType, Exception, string>)It.IsAny<object>())以及处理参数的 IInvocation 。

  • 天啊。我真的希望这个能发挥作用。也许这是我的版本..但我现在得到:“FormattedLogValues 由于其保护级别而无法访问”:( 这是我悲伤的脸。仅供参考,我有“ &lt;PackageReference Include="Moq" Version="4.13.1" /&gt; “如果您有机会(也许可以看看截止点在哪里),也许可以考虑发布您的最小起订量版本,谢谢。 (2认同)

Can*_*kut 5

这就是我对Moq (v4.10.1) 框架的解决方法。

public static class TestHelper
{ 

    public static Mock<ILogger<T>> GetMockedLoggerWithAutoSetup<T>()
    {
        var logger = new Mock<ILogger<T>>();

        logger.Setup<object>(x => x.Log(
       It.IsAny<LogLevel>(),
       It.IsAny<EventId>(),
       It.IsAny<object>(),
       It.IsAny<Exception>(),
       It.IsAny<Func<object, Exception, string>>()));

        return logger;
    }

    public static void VerifyLogMessage<T>(Mock<ILogger<T>> mockedLogger, LogLevel logLevel, Func<string, bool> predicate, Func<Times> times)
    {
        mockedLogger.Verify(x => x.Log(logLevel, 0, It.Is<object>(p => predicate(p.ToString())), null, It.IsAny<Func<object, Exception, string>>()), times);
    }
}
Run Code Online (Sandbox Code Playgroud)

——

public class Dummy
{

}

[Fact]
public void Should_Mock_Logger()
{
    var logger = TestHelper.GetMockedLoggerWithAutoSetup<Dummy>();
    logger.Object.LogInformation("test");
    TestHelper.VerifyLogMessage<Dummy>(logger, LogLevel.Information, msg => msg == "test", Times.Once);
}
Run Code Online (Sandbox Code Playgroud)

——

事情是,

如果我选择了其他<TCustom><object>logger.Setup(),它会失败上Verify步说,0话费被用于制作x.Log<TCustom>和展示给打了一个电话x.Log<object>。所以我将我的通用记录器设置为模拟Log<object>(..)方法。