【问题标题】:How to Moq Mock a LoggerFactory in C# AspNet Core如何在 C# AspNet Core 中 Moq 模拟 LoggerFactory
【发布时间】:2017-12-26 13:02:34
【问题描述】:

我正在尝试为控制器操作编写一些单元测试。为此,我使用 XUnit 和 Moq。控制器在构造函数中注入了一个 ILoggerFactory。一个起订量如何进行测试?

我尝试为控制器类模拟一个 Logger,然后模拟 CreateLogger 以返回模拟 Logger,但是当调用 LogInformation() 函数时,我不断收到各种测试运行时 NullReferenceExceptions。

        //   Logger that yields only disappointment...          
        var mockLogger = new Mock<ILogger<JwtController>>();
        mockLogger.Setup(ml => ml.Log(It.IsAny<LogLevel>(), It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
        var mockLoggerFactory = new Mock<ILoggerFactory>();
        mockLoggerFactory.Setup(mlf => mlf.CreateLogger("JwtController")).Returns(mockLogger.Object);

我假设问题是正在调用 LogInformation,这是一个扩展方法,那么如何起订量呢?

【问题讨论】:

  • 我们通常将ILogger&lt;SampleController&gt;注入控制器。注入ILoggerFactory的原因是什么?
  • 我可以试一试。它不会仍然使用扩展方法并因此仍然失败吗?
  • 有效!如果我将注入的对象更改为 ILogger,我可以使用“var mockLogger = new Mock>();”模拟记录器然后将 mockLogger.Object 传递给测试控制器。如果您@Win 发布答案,我很乐意接受。
  • 由于问题被标记为重复问题虽然the duplicate answer 是一个死胡同,我无法再回答了。我很高兴它解决了问题。
  • @stuartd - 请允许 Win 打开这个问题以提交我可以接受的答案?

标签: c# unit-testing moq


【解决方案1】:

为了它的价值:你也可以传递一个NullLoggerFactory 的实例,而不是模拟一个ILoggerFactory。这个NullLoggerFactory 将返回NullLogger 的实例。根据文档,这是一个:

什么都不做的简约记录器。

【讨论】:

  • 很好的答案!正是测试所需的内容!
  • 这应该是公认的答案,究竟应该如何进行测试!
  • 这也让我通过了测试,应该被接受
  • 我们可以验证记录到NullLogger 实例或NullLoggerFactory 实例创建的ILogger 的信息吗?这是使用模拟 ILogger 的好处之一,我不得不失去它。
【解决方案2】:

我只是模拟如下 ILogger 扩展方法,并在 ILoggerFactory 设置中使用返回 Mock ILogger 对象的值函数。

var mockLogger = new Mock<ILogger<[YOUR_CLASS_TYPE]>>();
mockLogger.Setup(
    m => m.Log(
        LogLevel.Information,
        It.IsAny<EventId>(),
        It.IsAny<object>(),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()));

var mockLoggerFactory = new Mock<ILoggerFactory>();
mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(() => mockLogger.Object);

这将返回您模拟的 Logger,您可以验证对其进行的任何调用。无需编写包装器或帮助器。

您甚至可以模拟 IsEnabled,这对于一些利用该功能的代码是必需的。

        mockLogger.Setup(
            m => m.IsEnabled(
                Microsoft.Extensions.Logging.LogLevel.Debug)).Returns(true);

因为只有一种方法可以模拟,(并且您必须调用),所以下面显示了您(可能)使用的日志记录调用,以使所有内容都指向这个确切的方法。

 catch (ArgumentOutOfRangeException argEx)
{
    // this.logger.LogError(argEx, argEx.Message); /* << this is what you would like to do, BUT it is an extension method, NOT (easily) mockable */
    Func<object, Exception, string> returnAnEmptyStringFunc = (a, b) => string.Empty;
    this.logger.Log(LogLevel.Error, ushort.MaxValue, argEx.Message, argEx, returnAnEmptyStringFunc);
    throw argEx;
}

【讨论】:

  • 所以我刚刚在我的代码中使用此方法而不是 NullLoggerFactory 发现了 2 个异常。我有一些看起来像这样的代码: if (this.Logger.IsEnabled(LogLevel.Debug)) { /* do something */ } 所以这是我现在首选的方法。谢谢瑞恩内特。不知道为什么以前没有人见过这个。我正在为您的答案添加一个小花絮,我希望没关系。我可以“获取”(又名“测试”)if (this.Logger.IsEnabled(LogLevel.Debug)) 块中的代码的唯一方法是通过此方法。 Aka,使用 NullLoggerFactory ,一些代码无法访问
  • 我回到了这个答案!对于未来的读者来说,关键之一是 Ryannet 模拟的方法似乎是唯一具体/真实的方法。 (与任何静态扩展方法相反,其中扩展方法(同样是静态的)......不容易被模拟。你可以在这个 url 看到这个。请注意,我们大多数人可能正在使用扩展方法而没有意识到它(LogInformation、LogError 等)。这是链接:docs.microsoft.com/en-us/dotnet/api/…
  • 我做了另一个编辑。我希望它增加了答案。这是一个相当小的谜题。
【解决方案3】:

对于任何需要这个问题的答案而不是变通方法的人,我扩展了本文的工作:

https://ardalis.com/testing-logging-in-aspnet-core

包装您使用的日志框架的任何部分,这无论如何都是个好主意。从您自己的日志记录界面开始:

public interface ILog<T>
{
    void LogError(Exception ex, string message, params object[] args);
    void LogInformation(string message, params object[] args);
}

然后添加实现以传递对框架的调用:

public class Log<T> : ILog<T>
{
    private readonly ILogger<T> logger;

    public Log(ILogger<T> logger)
    {
        this.logger = logger;
    }

    public void LogError(Exception ex, string message, params object[] args) => this.logger.LogError(ex, message, args);
    public void LogInformation(string message, params object[] args) => this.logger.LogInformation(message, args);
}

继续,添加一个用于包装记录器工厂的接口:

public interface ILogFactory
{
    ILog<T> CreateLog<T>();
}

以及实现:

public class LogFactory : ILogFactory
{
    private readonly ILoggerFactory loggerFactory;

    public LogFactory()
    {
        this.loggerFactory = new LoggerFactory();
    }

    public ILog<T> CreateLog<T>() => new Log<T>(new Logger<T>(this.loggerFactory));
}

这些是您应该引用Microsoft.Extensions.Logging 命名空间的唯一地方。在其他地方,使用您的ILog&lt;T&gt; 代替ILogger&lt;T&gt; 和您的ILogFactory 代替ILoggerFactory。您通常会在哪里依赖注入 LoggerFactory,而不是注入包装器:

IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ILogFactory>(new LogFactory());

在您的主代码中,您可以检索此 LogFactory 并为您的班级创建特定的 Log&lt;T&gt;

public class MyClass
{
    public void MyMethod(IServiceCollection serviceCollection)
    {
        var serviceProvider = serviceCollection.BuildServiceProvider();
        var logFactory = this.serviceProvider.GetRequiredService<ILogFactory>();
        var log = logFactory.CreateLog<ServiceApplication>();
        log.LogInformation("Hello, World!");
    }
}

您可以想象根据需要将MyMethod 的参数从IServiceCollection 更改为ILogFactoryILog&lt;MyClass&gt;。而且 - 重点是 - 你现在可以模拟上面的代码:

[Fact]
public void Test()
{
    IServiceCollection serviceCollection = new ServiceCollection();
    var mockLog = new Mock<ILog<MyClass>>();
    var mockLogFactory = new Mock<ILogFactory>();
    mockLogFactory.Setup(f => f.CreateLog<MyClass>()).Returns(mockLog.Object);
    serviceCollection.AddSingleton<ILogFactory>(mockLogFactory.Object);

    var myClass = new MyClass();
    myClass.MyMethod(serviceCollection);

    mockLog.Verify(l => l.LogInformation("Hello, World!"), Times.Once);
}

“根据您在整个应用程序中无法控制的类型,会增加耦合并经常成为问题和技术债务的来源。” - 史蒂夫史密斯

【讨论】:

    猜你喜欢
    • 2018-10-22
    • 2019-06-11
    • 1970-01-01
    • 2013-12-23
    • 1970-01-01
    • 2017-08-06
    • 2016-08-06
    相关资源
    最近更新 更多