如何在C#AspNet Core中模拟LoggerFactory

Ste*_*ert 7 c# unit-testing moq

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

我已经尝试为控制器类模拟一个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);
Run Code Online (Sandbox Code Playgroud)

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

Ben*_*min 10

物有所值:除了嘲笑an之外ILoggerFactory,您还可以传递的实例NullLoggerFactory。这NullLoggerFactory将返回的实例NullLogger。根据文档,这是:

简约的记录器,什么也不做。

  • 我们可以验证记录到“NullLogger”实例或由“NullLoggerFactory”实例创建的“ILogger”的信息吗?这是使用模拟的“ILogger”的好处之一,但我不得不失去它。 (4认同)
  • 这应该是公认的答案,确切地说应该如何完成测试目的! (3认同)

小智 10

我只是模拟下面的 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);
Run Code Online (Sandbox Code Playgroud)

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

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

        mockLogger.Setup(
            m => m.IsEnabled(
                Microsoft.Extensions.Logging.LogLevel.Debug)).Returns(true);
Run Code Online (Sandbox Code Playgroud)

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

 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;
}
Run Code Online (Sandbox Code Playgroud)


Mar*_*ark 7

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

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);
}
Run Code Online (Sandbox Code Playgroud)

然后添加实现以通过对框架的调用:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

public interface ILogFactory
{
    ILog<T> CreateLog<T>();
}
Run Code Online (Sandbox Code Playgroud)

并执行:

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));
}
Run Code Online (Sandbox Code Playgroud)

这些是您应仅引用Microsoft.Extensions.Logging名称空间的地方。在其他地方,使用您的ILog<T>代替ILogger<T>和您的ILogFactory代替ILoggerFactory。通常在依赖项注入的地方LoggerFactory,而是注入包装器:

IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ILogFactory>(new LogFactory());
Run Code Online (Sandbox Code Playgroud)

在您的主代码中,您可以检索此代码LogFactoryLog<T>为您的班级创建特定的代码:

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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以想象根据需要更改参数MyMethodfrom IServiceCollectionILogFactoryor ILog<MyClass>。而且-重点是-您现在可以使用以下方法模拟上面的代码:

[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);
}
Run Code Online (Sandbox Code Playgroud)

“取决于您在整个应用程序中无法控制的类型,这会增加耦合,并且经常成为问题和技术负担的来源。” -史蒂夫·史密斯