C# 如何模拟ILogger登录信息

C# 如何模拟ILogger登录信息,c#,logging,mocking,C#,Logging,Mocking,我有一个接收ILogger的类,我想模拟LogInformation调用,但这是一个扩展方法。如何对此进行适当的设置调用?ILogger通常通过扩展方法、LogWarning、LogError等使用 在我的例子中,我对LogWarning方法感兴趣,它在查看代码后从ILogger调用Log方法。 为了用Moq来模拟它,我最终做了以下几点: var list = new List<string>(); var logger = new Mock

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

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());
                    });

请注意特殊的强制转换:
(Func)It.IsAny()
,以及处理参数的IInvocation。

这就是我如何处理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);
    }
}
公共静态类TestHelper
{ 
公共静态模拟GetMockedLoggerWithAutoSetup()
{
var logger=newmock();
logger.Setup(x=>x.Log(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny());
返回记录器;
}
public static void VerifyLogMessage(Mock-mockedLogger、LogLevel-LogLevel、Func谓词、Func-times)
{
验证(x=>x.Log(logLevel,0,It.Is(p=>predicate(p.ToString())),null,It.IsAny()),次);
}
}
--

公共类虚拟
{
}
[事实]
public void应该\u Mock\u Logger()
{
var logger=TestHelper.GetMockedLoggerWithAutoSetup();
logger.Object.LogInformation(“测试”);
TestHelper.VerifyLogMessage(logger,LogLevel.Information,msg=>msg==“test”,Times.Once);
}
--

问题是


如果我为
logger.Setup()
选择了除
以外的任何
,它将在
验证
步骤中失败,说明
x.Log
进行了0次调用,并显示对
x.Log
的调用。因此,我将我的通用记录器设置为mock
Log(..)
方法。

如果您使用的是Moq>=4.13,下面是一种模拟
ILogger
的方法:

logger.Verify(x=>x.Log(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny(),
(Func)It.IsAny());
您可以将
It.IsAny()
It.IsAny()
It.IsAny()
存根更改为更具体的存根,但必须使用
It.IsAnyType
,因为
FormattedLogValues
现在是
内部的


参考资料:

使用Moq 4.14.5测试的回调示例。更多关于这个的信息

var logger=new Mock();
logger.Setup(x=>x.Log(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny(),
(Func)It.IsAny())
.Callback(新调用操作)(调用=>
{
var logLevel=(logLevel)invocation.Arguments[0];//前两个参数始终是上述设置中指定的参数
var eventId=(eventId)invocation.Arguments[1];//所以我不确定您是否真的想要使用它们
var state=invocation.Arguments[2];
var exception=(exception?)invocation.Arguments[3];
var formatter=invocation.Arguments[4];
var invokeMethod=formatter.GetType().GetMethod(“Invoke”);
var logMessage=(字符串)invokeMethod?.Invoke(格式化程序,新[]{state,exception});
}));
用于单元测试的完整通用帮助器类

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;
    }
}
公共静态类LoggerHelper
{
公共静态模拟GetLogger()
{
var logger=newmock();
logger.Setup(x=>x.Log(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny(),
(Func)It.IsAny())
.Callback(新调用操作)(调用=>
{
var logLevel=(logLevel)invocation.Arguments[0];//前两个参数始终是上述设置中指定的参数
var eventId=(eventId)invocation.Arguments[1];//所以我不确定您是否真的想要使用它们
var state=invocation.Arguments[2];
var exception=(exception?)invocation.Arguments[3];
var formatter=invocation.Arguments[4];
var invokeMethod=formatter.GetType().GetMethod(“Invoke”);
var logMessage=(字符串)invokeMethod?.Invoke(格式化程序,新[]{state,exception});
Trace.WriteLine(日志消息);
}));
返回记录器;
}
}

在这种情况下,双重测试等级可能比最低起订量更容易。创建它需要做的工作稍微多一些,但是你可以永远重复使用它,而且它比Moq回调更容易阅读和使用。(我喜欢最小起订量,但没有更简单的方法。)

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

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

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

    // Add more of these if they make life easier.
    public IEnumerable<LogEntry> InformationEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Information);

    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; }
}
使用系统;
使用System.Collections.Generic;
使用System.Linq;
使用Microsoft.Extensions.Logging;
公共类LoggerDouble:ILogger,ILogger
{
公共列表日志条目{get;}=new List();
//如果它们能让生活更轻松的话,可以添加更多。
公共IEnumerable InformationEntries=>
其中(e=>e.LogLevel==LogLevel.Information);
公共无效日志(日志级别、日志级别、事件ID、事件ID、TState状态、异常、函数格式化程序)
{
添加(新的日志条目(logLevel、eventId、state、exception));
}
公共布尔值已启用(日志级别日志级别)
{
返回true;
}
公共IDisposable BeginScope(州)
{
返回新的LoggingScope();
}
公共类日志作用域:IDisposable
{
公共空间处置()
{
}
}
}
公共类日志条目
{
公共日志条目(日志级别、日志级别、事件ID、事件ID、对象状态、异常)
{
LogLevel=LogLevel;
EventId=EventId;
状态=状态;
例外=例外;
}
公共日志级别日志级别{get;}
公共事件ID事件ID{get;}
公共对象状态{get;}
公共异常{get;}
}
创建一个实例并将其作为记录器注入测试类。然后你就可以看到这些物体了
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);
}
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 });
    }));
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;
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;

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

    // Add more of these if they make life easier.
    public IEnumerable<LogEntry> InformationEntries =>
        LogEntries.Where(e => e.LogLevel == LogLevel.Information);

    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; }
}