使用FakeItEasy

问题描述

我正在为自己设计一个有趣的项目,以学习火焰,许多核心概念以及常规知识。

我正在尝试为FakeItEasy实现记录程序扩展,以便更轻松地测试Microsoft的ILogger。 基于https://dev.azure.com/BrideButler/Bride%20Butler%20Open%20Source/_git/FakeItEasy.DotnetCore.LoggerExtensions?path=%2FFakeItEasy.DotnetCore.LoggerExtensions%2FLoggerExtensions.cs,但现在尝试使其适用于.NET 5。 现在FormattedLogValues是内部代码,此代码将不再起作用。

目标是在我的测试中添加类似的内容(而不使用Moq) _logger.VerifyLog(LogLevel.@R_749_4045@ion,"Get Weather Called").MustHaveHappenedOnceExactly();A.CallTo(() => _logger.Log(LogLevel.@R_749_4045@ion,"Get Weather Called").MustHaveHappenedOnceExactly();不一定要有语法糖,但我想让它与FakeItEasy而不是Moq一起使用。

下面是所有涉及的代码。当然,我在第一行Microsoft.Extensions.Logging.ILogger.Log 1 [System.Object] vs Microsoft.Extensions.Logging.ILogger 1[Server.Controllers.WeatherForecastController].Log 1 [Microsoft.Extensions.Logging.FormattedLogValues]中看到了一些区别,但是我我不知道如何解决此问题,因为我不完全了解出了什么问题。

我的问题显然是,我应该怎么做才能修复它,但同时我也好奇我缺少了什么部分。

我的错误如下:

  Assertion Failed for the following call:
        Microsoft.Extensions.Logging.ILogger.Log`1[System.Object]
        (
            logLevel: @R_749_4045@ion,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: @R_749_4045@ion,eventId: 0,state: [[{OriginalFormat},Get Weather Called]],exception: NULL,formatter: System.Func`3[Microsoft.Extensions.Logging.FormattedLogValues,System.Exception,System.String]
      )

测试: (使用xUnit,FakeItEasy,Fixture)

LoggerExtensions

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,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,string message)
        {
            return A.CallTo(() => logger.Log(
                    level,A<EventId>._,A<object>.That.Matches(e => e.ToString().Contains(message)),A<Exception>._,A<Func<object,Exception,string>>._)
                );
        }
    }

TestConstructor:

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.@R_749_4045@ion,"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.@R_749_4045@ion,"Get Weather Called");
    }
}

更新: 找到的选项

仅测试是否以正确的日志级别调用记录器:

   public static IVoidArgumentValidationConfiguration VerifyLog<T>(this ILogger<T> logger,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

解决方法

阅读https://stackoverflow.com/users/4728685/pavel-anikhouski对帖子的评论后。我“明白”出了什么问题。进入了无数主题,并提出了一种可能“使用”更好的编码标准的解决方案。希望它对其他人有帮助!

编辑:一些解释。

找到了this的github注释,它是解决方案的基础。然后找到this github pr,他们提到将Microsoft.Extensions.Logging.FormattedLogValues更改为IReadOnlyList<KeyValuePair<string,object>> 可以从GetArgument(2)中提取出来。 (来源:https://stackoverflow.com/a/63701968/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,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,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;
        }
    }