C# 用假动作测试ILogger

C# 用假动作测试ILogger,c#,fakeiteasy,ilogger,C#,Fakeiteasy,Ilogger,我正在为自己做一个有趣的项目来学习blazor,很多核心概念和一般实践 我正在尝试为Fakeitesy实现记录器扩展,以便更容易地测试MicrosoftsiLogger。 基于.NET5,但现在正在努力使其适用于.NET5。 既然FormattedLogValues是内部的,那么这个代码就不再工作了 我们的目标是在我的测试中有这样的东西(而不是使用Moq) \u logger.VerifyLog(LogLevel.Information,“调用天气”)。必须准确地执行() 或者A.CallTo(

我正在为自己做一个有趣的项目来学习blazor,很多核心概念和一般实践

我正在尝试为Fakeitesy实现记录器扩展,以便更容易地测试MicrosoftsiLogger。 基于.NET5,但现在正在努力使其适用于.NET5。 既然
FormattedLogValues
是内部的,那么这个代码就不再工作了

我们的目标是在我的测试中有这样的东西(而不是使用Moq)
\u logger.VerifyLog(LogLevel.Information,“调用天气”)。必须准确地执行()
或者
A.CallTo(()=>_logger.Log(LogLevel.Information,“Get Weather Called”)。musthavehappenedonecess();
它不必有语法上的糖分,但我想让它与FakeItEasy而不是Moq一起工作

下面是所有涉及到的代码。当然,我在第一行看到了一些不同之处
Microsoft.Extensions.Logging.ILogger.Log
1[System.Object]
vs
Microsoft.Extensions.Logging.ILogger
1[Server.Controllers.WeatherForecastController]。Log
1[Microsoft.Extensions.Logging.formatedLogvalues]`但我不知道如何“修复”这是因为我不完全明白出了什么问题

我的问题很明显,我应该怎么做来修复它,但我也很好奇我遗漏了什么部分

我的错误如下:

  Assertion failed for the following call:
        Microsoft.Extensions.Logging.ILogger.Log`1[System.Object]
        (
            logLevel: Information,
            eventId: <Ignored>, 
            state: <e => e.ToString().Contains(value(Server.Tests.Extensions.LoggerExtensions+<>c__DisplayClass2_0`1[Server.Controllers.WeatherForecastController]).message)>,
            exception: <Ignored>, 
            formatter: <Ignored>
        )
      Expected to find it once exactly but didn't find it among the calls:
        1: Microsoft.Extensions.Logging.ILogger`1[Server.Controllers.WeatherForecastController].Log`1[Microsoft.Extensions.Logging.FormattedLogValues]
        (
            logLevel: Information,
            eventId: 0,
            state: [[{OriginalFormat}, Get Weather Called]],
            exception: NULL,
            formatter: System.Func`3[Microsoft.Extensions.Logging.FormattedLogValues,System.Exception,System.String]
      )
控制器:

public class WeatherForecastController : ControllerBase
{
    private readonly ILogger _logger;

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

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        // Left out other code then logging
        _logger.Log(LogLevel.Information, "Get Weather Called");
    }
}
在阅读了这篇文章的评论后,我“明白”了问题所在。我浏览了无数的主题,提出了一个“可行”的解决方案,也许可以使用更好的编码标准。希望它能帮助其他人

编辑:一些解释

找到了作为解决方案基础的github注释。然后找到了github pr,其中他们提到要将Microsoft.Extensions.Logging.FormattedLogValues更改为
IReadOnlyList
这可以从
GetArgument(2)
中提取(来源:)

公共静态类LoggerExtensions
{
公共静态无效验证日志必须已发生(此ILogger记录器、日志级别、字符串消息)
{
尝试
{
logger.VerifyLog(级别、消息)
.一定发生过();
}
捕获(例外e)
{
使用消息“{message}\”,e验证对日志的调用时抛出新的ExpectationException($);
}
}
public static void VerifyLogMustNothaveOccessed(此ILogger记录器,日志级别,字符串消息)
{
尝试
{
logger.VerifyLog(级别、消息)
.mustnothavebefored();
}
捕获(例外e)
{
使用消息“{message}\”,e验证对日志的调用时抛出新的ExpectationException($);
}
}
公共静态IVoidArgumentValidationConfiguration VerifyLog(此ILogger记录器,日志级别,字符串消息)
{
返回A.CallTo(记录器)
.Where(call=>call.Method.Name==“Log”
&&call.GetArgument(0)=级别
&&CheckLogMessages(call.GetArgument(2),message));
}
私有静态bool CheckLogMessages(IReadOnlyList readonlyList,字符串消息)
{
foreach(ReadOnlyList中的var kvp)
{
if(kvp.Value.ToString()包含(消息))
{
返回true;
}
}
返回false;
}
}

您的错误是,
格式化程序
委托(日志
方法中的最后一个参数)应该是
Func
。但是您在这里使用的是
Func
,它可能是issue@PavelAnikhouski谢谢你的解释。由于理解你评论中的错误,我做了一个变通。
private readonly IFixture _fixture;
private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastControllerTests()
{
    _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization());
    _logger = _fixture.Freeze<Fake<ILogger<WeatherForecastController>>>().FakedObject;
}
[Fact]
public void WeatherForecast_Get_Should_Log()
{
    // Arrange
    var weatherController = new WeatherForecastController(_logger);

    // Act
    weatherController.Get();

    // Assert
    _logger.VerifyLog(LogLevel.Information, "Get Weather Called").MustHaveHappenedOnceExactly();
}

public class WeatherForecastController : ControllerBase
{
    private readonly ILogger _logger;

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

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        // Left out other code then logging
        _logger.Log(LogLevel.Information, "Get Weather Called");
    }
}
   public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
        {
            return A.CallTo(logger).Where(call => call.Method.Name == "Log" && call.GetArgument<LogLevel>(0) == level);
        }
https://stackoverflow.com/a/59182490/1112413
https://stackoverflow.com/a/56728528/1112413
  public static class LoggerExtensions
    {
        public static void VerifyLogMustHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
        {
            try
            {
                logger.VerifyLog(level, message)
                    .MustHaveHappened();
            }
            catch (Exception e)
            {
                throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
            }
        }

        public static void VerifyLogMustNotHaveHappened<T>(this ILogger<T> logger, LogLevel level, string message)
        {
            try
            {
                logger.VerifyLog(level, message)
                    .MustNotHaveHappened();
            }
            catch (Exception e)
            {
                throw new ExpectationException($"while verifying a call to log with message: \"{message}\"", e);
            }
        }

        public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger, LogLevel level, string message)
        {
            return A.CallTo(logger)
                .Where(call => call.Method.Name == "Log" 
                    && call.GetArgument<LogLevel>(0) == level 
                    && CheckLogMessages(call.GetArgument<IReadOnlyList<KeyValuePair<string, object>>>(2), message));
        }

        private static bool CheckLogMessages(IReadOnlyList<KeyValuePair<string, object>> readOnlyLists, string message)
        {
            foreach(var kvp in readOnlyLists)
            {
                if (kvp.Value.ToString().Contains(message))
                {
                    return true;
                }
            }

            return false;
        }
    }