如何在 ASP.NET Core 中使用 ILogger 进行单元测试

Posted

技术标签:

【中文标题】如何在 ASP.NET Core 中使用 ILogger 进行单元测试【英文标题】:How to unit test with ILogger in ASP.NET Core 【发布时间】:2017-09-11 10:54:52 【问题描述】:

这是我的控制器:

public class BlogController : Controller

    private IDAO<Blog> _blogDAO;
    private readonly ILogger<BlogController> _logger;

    public BlogController(ILogger<BlogController> logger, IDAO<Blog> blogDAO)
    
        this._blogDAO = blogDAO;
        this._logger = logger;
    
    public IActionResult Index()
    
        var blogs = this._blogDAO.GetMany();
        this._logger.LogInformation("Index page say hello", new object[0]);
        return View(blogs);
    

如您所见,我有 2 个依赖项,一个 IDAO 和一个 ILogger

这是我的测试类,我使用 xUnit 进行测试,使用 Moq 创建模拟和存根,我可以轻松模拟 DAO,但使用 ILogger 我不知道该怎么做,所以我只是传递 null并在运行测试时注释掉登录控制器的调用。有没有办法测试但仍然以某种方式保留记录器?

public class BlogControllerTest

    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        var controller = new BlogController(null,mockRepo.Object);

        var result = controller.Index();

        var viewResult = Assert.IsType<ViewResult>(result);
        var model = Assert.IsAssignableFrom<IEnumerable<Blog>>(viewResult.ViewData.Model);
        Assert.Equal(2, model.Count());
    

【问题讨论】:

您可以使用模拟作为存根,正如 Ilya 建议的那样,如果您实际上并未尝试测试是否调用了日志记录方法本身。如果是这种情况,模拟记录器不起作用,您可以尝试几种不同的方法。我写了一个short article 展示了各种方法。文章包括a full GitHub repo with each of the different options。最后,我的建议是使用您自己的适配器,而不是直接使用 ILogger 类型,如果您需要能够 正如@ssmith 提到的,在验证ILogger 的实际调用时存在一些问题。他的博文中有一些很好的建议,我的解决方案似乎解决了answer below 中的大部分问题。 【参考方案1】:

只需模拟它以及任何其他依赖项:

var mock = new Mock<ILogger<BlogController>>();
ILogger<BlogController> logger = mock.Object;

//or use this short equivalent 
logger = Mock.Of<ILogger<BlogController>>()

var controller = new BlogController(logger);

您可能需要安装Microsoft.Extensions.Logging.Abstractions 软件包才能使用ILogger&lt;T&gt;

此外,您还可以创建一个真正的记录器:

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();

【讨论】:

在工厂上调用 AddDebug() 记录到调试输出窗口: var factory = serviceProvider.GetService().AddDebug(); 我发现“真正的记录器”方法更有效! 真正的 logger 部分也非常适合在特定场景下测试 LogConfiguration 和 LogLevel。 这种方法只允许存根,但不允许验证调用。我的解决方案似乎解决了answer below 中的大部分验证问题。 从 .net 5 开始,AddDebug 应该在 AddLogging 中调用,而不是从 ILoggerFactory 调用。 new ServiceCollection().AddLogging(builder =&gt; builder.AddDebug())...【参考方案2】:

实际上,我发现Microsoft.Extensions.Logging.Abstractions.NullLogger&lt;&gt; 看起来是一个完美的解决方案。安装包Microsoft.Extensions.Logging.Abstractions,然后按照示例配置使用:

using Microsoft.Extensions.Logging;

public void ConfigureServices(IServiceCollection services)

    ...

    services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

    ...

using Microsoft.Extensions.Logging;

public class MyClass : IMyClass

    public const string ErrorMessageILoggerFactoryIsNull = "ILoggerFactory is null";

    private readonly ILogger<MyClass> logger;

    public MyClass(ILoggerFactory loggerFactory)
    
        if (null == loggerFactory)
        
            throw new ArgumentNullException(ErrorMessageILoggerFactoryIsNull, (Exception)null);
        

        this.logger = loggerFactory.CreateLogger<MyClass>();
    

和单元测试

//using Microsoft.VisualStudio.TestTools.UnitTesting;
//using Microsoft.Extensions.Logging;

[TestMethod]
public void SampleTest()

    ILoggerFactory doesntDoMuch = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
    IMyClass testItem = new MyClass(doesntDoMuch);
    Assert.IsNotNull(testItem);
   

【讨论】:

这似乎只适用于 .NET Core 2.0,不适用于 .NET Core 1.1。 @adospace,你的评论比答案更有用 你能举个例子来说明它是如何工作的吗?单元测试时,我希望日志出现在输出窗口中,我不确定是否这样做。 @adospace 这应该放在 startup.cs 中吗? @raklos hum,不,它应该用于实例化 ServiceCollection 的测试中的启动方法【参考方案3】:

更新(感谢@Gopal Krishnan 的评论):

使用 Moq >= 4.15.0,以下代码可以正常工作(不再需要演员表):

 loggerMock.Verify(
                x => x.Log(
                    LogLevel.Information,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    It.IsAny<Exception>(),
                    It.IsAny<Func<It.IsAnyType, Exception, string>>()),
                Times.Once);

以前版本的答案(起订量

对于使用 Moq 的 .net core 3 答案

https://***.com/a/54646657/2164198 https://***.com/a/54809607/2164198 https://***.com/a/56728528/2164198

由于问题 TState in ILogger.Log used to be object, now FormattedLogValues 中描述的更改而不再工作

幸运的是 stakx 提供了一个不错的 workaround。所以我发布它是希望它可以为其他人节省时间(花了一段时间才弄清楚):

 loggerMock.Verify(
                x => x.Log(
                    LogLevel.Information,
                    It.IsAny<EventId>(),
                    It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                    It.IsAny<Exception>(),
                    (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
                Times.Once);

【讨论】:

你拯救了我的一天..谢谢。 我希望能够将(Func&lt;It.IsAnyType, Exception, string&gt;) It.IsAny&lt;object&gt;()) 替换为It.IsAny&lt;Func&lt;It.IsAnyType, Exception, string&gt;&gt;()),但它不起作用。这是什么魔法:-)? 看起来我只需要更新到最新版本的起订量,v4.15 添加了这个功能github.com/moq/moq4/issues/918#issuecomment-720090418 对于起订量 It.IsAnyType:_loggerMock.Verify(x =&gt; x.Log(LogLevel.Information, It.IsAny&lt;EventId&gt;(), It.Is&lt;object&gt;(o =&gt; string.Equals($"Some log message", o.ToString(), StringComparison.InvariantCultureIgnoreCase)), It.IsAny&lt;Exception&gt;(), (Func&lt;object, Exception, string&gt;)It.IsAny&lt;object&gt;()), Times.Once); 非常感谢,您的回答节省了我的时间。【参考方案4】:

使用自定义记录器,该记录器使用ITestOutputHelper(来自 xunit)来捕获输出和日志。以下是仅将state 写入输出的小示例。

public class XunitLogger<T> : ILogger<T>, IDisposable

    private ITestOutputHelper _output;

    public XunitLogger(ITestOutputHelper output)
    
        _output = output;
    
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    
        _output.WriteLine(state.ToString());
    

    public bool IsEnabled(LogLevel logLevel)
    
        return true;
    

    public IDisposable BeginScope<TState>(TState state)
    
        return this;
    

    public void Dispose()
    
    

在你的单元测试中使用它

public class BlogControllerTest

  private XunitLogger<BlogController> _logger;

  public BlogControllerTest(ITestOutputHelper output)
    _logger = new XunitLogger<BlogController>(output);
  

  [Fact]
  public void Index_ReturnAViewResult_WithAListOfBlog()
  
    var mockRepo = new Mock<IDAO<Blog>>();
    mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
    var controller = new BlogController(_logger,mockRepo.Object);
    // rest
  

【讨论】:

嗨。这对我来说很好。现在我如何检查或查看我的日志信息 我直接从 VS 运行单元测试用例。我没有控制台 @maliksaifullah 我正在使用 resharper。让我用 vs 检查一下 @maliksaifullah,VS 的 TestExplorer 提供了一个打开测试输出的链接。在 TestExplorer 中选择您的测试,底部有一个链接 这太好了,谢谢!一些建议:1)这不需要是通用的,因为没有使用类型参数。仅实现ILogger 将使其更广泛地可用。 2) BeginScope 不应返回自身,因为这意味着在运行期间开始和结束范围的任何测试方法都将处理记录器。相反,创建一个实现 IDisposable 的私有“虚拟”嵌套类并返回其实例(然后从 XunitLogger 中删除 IDisposable)。【参考方案5】:

加上我的 2 美分,这是一个辅助扩展方法,通常放在静态辅助类中:

static class MockHelper

    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    
        return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    
        mock.Verify(Verify<T>(level), times);
    

然后,你可以这样使用它:

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

当然,您可以轻松扩展它以模拟任何期望(即期望、消息等……)

【讨论】:

这是一个非常优雅的解决方案。 我同意,这个答案非常好。我不明白为什么它没有那么多票 工厂。这是非通用 ILogger 的版本:gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb 是否可以创建模拟来检查我们在 LogWarning 中传递的字符串?例如:It.Is&lt;string&gt;(s =&gt; s.Equals("A parameter is empty!")) 这很有帮助。对我来说缺少的一个部分是如何在写入 XUnit 输出的模拟上设置回调?从来没有为我打过回调。【参考方案6】:

在@ivan-samygin 和@stakx 的工作基础上更进一步,这里有一些扩展方法也可以匹配异常和所有日志值 (KeyValuePairs)。

这些工作(在我的机器上;))与 .Net Core 3、Moq 4.13.0 和 Microsoft.Extensions.Logging.Abstractions 3.1.0 一起工作。

/// <summary>
/// Verifies that a Log call has been made, with the given LogLevel, Message and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedLogLevel">The LogLevel to verify.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel expectedLogLevel, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)

    loggerMock.Verify(mock => mock.Log(
        expectedLogLevel,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()
        )
    );


/// <summary>
/// Verifies that a Log call has been made, with LogLevel.Error, Message, given Exception and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedException">The Exception to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, string expectedMessage, Exception expectedException, params KeyValuePair<string, object>[] expectedValues)

    loggerMock.Verify(logger => logger.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.Is<Exception>(e => e == expectedException),
        It.Is<Func<It.IsAnyType, Exception, string>>((o, t) => true)
    ));


private static bool MatchesLogValues(object state, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)

    const string messageKeyName = "OriginalFormat";

    var loggedValues = (IReadOnlyList<KeyValuePair<string, object>>)state;

    return loggedValues.Any(loggedValue => loggedValue.Key == messageKeyName && loggedValue.Value.ToString() == expectedMessage) &&
           expectedValues.All(expectedValue => loggedValues.Any(loggedValue => loggedValue.Key == expectedValue.Key && loggedValue.Value == expectedValue.Value));

【讨论】:

【参考方案7】:

如果仍然是实际的。在 .net core >= 3

的测试中记录输出的简单方法
[Fact]
public void SomeTest()

    using var logFactory = LoggerFactory.Create(builder => builder.AddConsole());
    var logger = logFactory.CreateLogger<AccountController>();
    
    var controller = new SomeController(logger);

    var result = controller.SomeActionAsync(new Dto ... ).GetAwaiter().GetResult();

【讨论】:

这很有用;虽然它不模拟该功能,但它允许在没有模拟记录器的情况下继续测试。 这种方法的另一个好处是,如果您将最低日志级别设置为调试,测试还将运行由 logger.IsEnabled(..) 保护的任何代码块,可能会捕获更多错误。【参考方案8】:

已经提到你可以像任何其他界面一样模拟它。

var logger = new Mock<ILogger<QueuedHostedService>>();

到目前为止一切顺利。

好消息是您可以使用Moq验证是否已执行某些调用。例如,在这里我检查日志是否已使用特定的Exception 调用。

logger.Verify(m => m.Log(It.Is<LogLevel>(l => l == LogLevel.Information), 0,
            It.IsAny<object>(), It.IsAny<TaskCanceledException>(), It.IsAny<Func<object, Exception, string>>()));

使用Verify 时,重点是针对来自ILooger 接口的真正Log 方法而不是扩展方法。

【讨论】:

【参考方案9】:

其他答案建议通过模拟ILogger 很容易,但是验证是否确实对记录器进行了调用突然变得更加困难。原因是大多数调用实际上并不属于ILogger 接口本身。

所以大多数调用是扩展方法,它们调用接口的唯一Log 方法。原因似乎是,如果您只有一个而不是很多重载归结为相同的方法,那么实现接口会更容易。

当然,缺点是要验证是否已拨打电话突然变得更加困难,因为您应该验证的电话与您拨打的电话非常不同。有一些不同的方法可以解决这个问题,我发现用于模拟框架的自定义扩展方法将使其最容易编写。

这是我使用NSubstitute 制作的方法示例:

public static class LoggerTestingExtensions

    public static void LogError(this ILogger logger, string message)
    
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    


这就是它的使用方法:

_logger.Received(1).LogError("Something bad happened");   

看起来就像您直接使用该方法一样,这里的技巧是我们的扩展方法获得优先级,因为它在命名空间中比原始方法“更接近”,因此将使用它。

不幸的是,它并没有提供我们想要的 100%,即错误消息不会那么好,因为我们不直接检查字符串,而是检查涉及字符串的 lambda,但 95% 总比没有好:) 此外,这种方法将使测试代码

附:对于 Moq,可以使用为 Mock&lt;ILogger&lt;T&gt;&gt; 编写扩展方法的方法,该方法执行 Verify 以实现类似的结果。

附言这在 .Net Core 3 中不再起作用,请查看此线程以获取更多详细信息:https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574

【讨论】:

为什么要验证记录器调用?它们不是业务逻辑的一部分。如果发生了不好的事情,我宁愿验证实际的程序行为(例如调用错误处理程序或抛出异常)而不是记录消息。 嗯,我认为测试它也很重要,至少在某些情况下是这样。我已经看到太多次程序静默失败,所以我认为在发生异常时验证日志记录是有意义的,例如它不像“非此即彼”,而是测试实际程序行为和日志记录。 'FormattedLogValues' 由于其保护级别而无法访问。无法在此处访问内部结构“FormattedLogValues”。 请查看最后的 P.P.S.【参考方案10】:

当使用 StructureMap / Lamar 时:

var c = new Container(_ =>

    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
);

文档:

https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.abstractions.nulllogger?view=aspnetcore-2.1 http://structuremap.github.io/generics/

【讨论】:

【参考方案11】:

仅仅创建一个虚拟的ILogger 对于单元测试来说不是很有价值。您还应该验证是否进行了日志记录调用。您可以使用Moq 注入模拟ILogger,但验证调用可能有点棘手。 This article 深入了解起订量验证。

这是文章中的一个非常简单的例子:

_loggerMock.Verify(l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), Times.Exactly(1));

它验证是否记录了信息消息。但是,如果我们想要验证关于消息的更复杂的信息,例如消息模板和命名属性,就变得更加棘手:

_loggerMock.Verify
(
    l => l.Log
    (
        //Check the severity level
        LogLevel.Error,
        //This may or may not be relevant to your scenario
        It.IsAny<EventId>(),
        //This is the magical Moq code that exposes internal log processing from the extension methods
        It.Is<It.IsAnyType>((state, t) =>
            //This confirms that the correct log message was sent to the logger. OriginalFormat should match the value passed to the logger
            //Note: messages should be retrieved from a service that will probably store the strings in a resource file
            CheckValue(state, LogTest.ErrorMessage, "OriginalFormat") &&
            //This confirms that an argument with a key of "recordId" was sent with the correct value
            //In Application Insights, this will turn up in Custom Dimensions
            CheckValue(state, recordId, nameof(recordId))
    ),
    //Confirm the exception type
    It.IsAny<NotImplementedException>(),
    //Accept any valid Func here. The Func is specified by the extension methods
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
    //Make sure the message was logged the correct number of times
    Times.Exactly(1)
);

我相信您可以对其他模拟框架做同样的事情,但ILogger 接口确保它很困难。

【讨论】:

我同意这种观点,正如您所说,构建表达式可能会有些困难。我经常遇到同样的问题,所以最近将 Moq.Contrib.ExpressionBuilders.Logging 放在一起,以提供一个流畅的界面,使其更加可口。【参考方案12】:

我尝试使用 NSubstitute 来模拟 Logger 接口(但失败了,因为Arg.Any&lt;T&gt;() 需要一个我无法提供的类型参数),但最终在以下方式:

    internal sealed class TestLogger<T> : ILogger<T>, IDisposable
    
        private readonly List<LoggedMessage> _messages = new List<LoggedMessage>();

        public IReadOnlyList<LoggedMessage> Messages => _messages;

        public void Dispose()
        
        

        public IDisposable BeginScope<TState>(TState state)
        
            return this;
        

        public bool IsEnabled(LogLevel logLevel)
        
            return true;
        

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        
            var message = formatter(state, exception);
            _messages.Add(new LoggedMessage(logLevel, eventId, exception, message));
        

        public sealed class LoggedMessage
        
            public LogLevel LogLevel  get; 
            public EventId EventId  get; 
            public Exception Exception  get; 
            public string Message  get; 

            public LoggedMessage(LogLevel logLevel, EventId eventId, Exception exception, string message)
            
                LogLevel = logLevel;
                EventId = eventId;
                Exception = exception;
                Message = message;
            
        
    

您可以轻松访问所有记录的消息并断言随它提供的所有有意义的参数。

【讨论】:

【参考方案13】:

我创建了一个包 Moq.ILogger,以使测试 ILogger 扩展更加容易。

您实际上可以使用以下更接近您的实际代码的内容。

loggerMock.VerifyLog(c => c.LogInformation(
                 "Index page say hello", 
                 It.IsAny<object[]>());

不仅编写新测试更容易,而且维护也是免费的。

可以在 here 找到 repo,并且还有一个 nuget 包 (Install-Package ILogger.Moq)。

我还在我的blog 上用一个真实的例子来解释它。

简而言之,假设您有以下代码:

public class PaymentsProcessor

    private readonly IOrdersRepository _ordersRepository;
    private readonly IPaymentService _paymentService;
    private readonly ILogger<PaymentsProcessor> _logger;

    public PaymentsProcessor(IOrdersRepository ordersRepository, 
        IPaymentService paymentService, 
        ILogger<PaymentsProcessor> logger)
    
        _ordersRepository = ordersRepository;
        _paymentService = paymentService;
        _logger = logger;
    

    public async Task ProcessOutstandingOrders()
    
        var outstandingOrders = await _ordersRepository.GetOutstandingOrders();
        
        foreach (var order in outstandingOrders)
        
            try
            
                var paymentTransaction = await _paymentService.CompletePayment(order);
                _logger.LogInformation("Order with orderReference was paid at by customerEmail, having transactionId", 
                                       order.OrderReference, 
                                       paymentTransaction.CreateOn, 
                                       order.CustomerEmail, 
                                       paymentTransaction.TransactionId);
            
            catch (Exception e)
            
                _logger.LogWarning(e, "An exception occurred while completing the payment for orderReference", 
                                   order.OrderReference);
            
        
        _logger.LogInformation("A batch of 0 outstanding orders was completed", outstandingOrders.Count);
    

然后您可以编写一些测试,例如

[Fact]
public async Task Processing_outstanding_orders_logs_batch_size()

    // Arrange
    var ordersRepositoryMock = new Mock<IOrdersRepository>();
    ordersRepositoryMock.Setup(c => c.GetOutstandingOrders())
        .ReturnsAsync(GenerateOutstandingOrders(100));

    var paymentServiceMock = new Mock<IPaymentService>();
    paymentServiceMock
        .Setup(c => c.CompletePayment(It.IsAny<Order>()))
        .ReturnsAsync((Order order) => new PaymentTransaction
        
            TransactionId = $"TRX-order.OrderReference"
        );

    var loggerMock = new Mock<ILogger<PaymentsProcessor>>();

    var sut = new PaymentsProcessor(ordersRepositoryMock.Object, paymentServiceMock.Object, loggerMock.Object);

    // Act
    await sut.ProcessOutstandingOrders();

    // Assert
    loggerMock.VerifyLog(c => c.LogInformation("A batch of 0 outstanding orders was completed", 100));


[Fact]
public async Task Processing_outstanding_orders_logs_order_and_transaction_data_for_each_completed_payment()

    // Arrange
    var ordersRepositoryMock = new Mock<IOrdersRepository>();
    ordersRepositoryMock.Setup(c => c.GetOutstandingOrders())
        .ReturnsAsync(GenerateOutstandingOrders(100));

    var paymentServiceMock = new Mock<IPaymentService>();
    paymentServiceMock
        .Setup(c => c.CompletePayment(It.IsAny<Order>()))
        .ReturnsAsync((Order order) => new PaymentTransaction
        
            TransactionId = $"TRX-order.OrderReference"
        );

    var loggerMock = new Mock<ILogger<PaymentsProcessor>>();

    var sut = new PaymentsProcessor(ordersRepositoryMock.Object, paymentServiceMock.Object, loggerMock.Object);

    // Act
    await sut.ProcessOutstandingOrders();

    // Assert
    loggerMock.VerifyLog(logger => logger.LogInformation("Order with orderReference was paid at by customerEmail, having transactionId",
        It.Is<string>(orderReference => orderReference.StartsWith("Reference")),
        It.IsAny<DateTime>(),
        It.Is<string>(customerEmail => customerEmail.Contains("@")),
        It.Is<string>(transactionId => transactionId.StartsWith("TRX"))),
      Times.Exactly(100));


[Fact]
public async Task Processing_outstanding_orders_logs_a_warning_when_payment_fails()

    // Arrange
    var ordersRepositoryMock = new Mock<IOrdersRepository>();
    ordersRepositoryMock.Setup(c => c.GetOutstandingOrders())
        .ReturnsAsync(GenerateOutstandingOrders(2));

    var paymentServiceMock = new Mock<IPaymentService>();
    paymentServiceMock
        .SetupSequence(c => c.CompletePayment(It.IsAny<Order>()))
        .ReturnsAsync(new PaymentTransaction
        
            TransactionId = "TRX-1",
            CreateOn = DateTime.Now.AddMinutes(-new Random().Next(100)),
        )
        .Throws(new Exception("Payment exception"));

    var loggerMock = new Mock<ILogger<PaymentsProcessor>>();

    var sut = new PaymentsProcessor(ordersRepositoryMock.Object, paymentServiceMock.Object, loggerMock.Object);

    // Act
    await sut.ProcessOutstandingOrders();

    // Assert
    loggerMock.VerifyLog(c => c.LogWarning(
                 It.Is<Exception>(paymentException => paymentException.Message.Contains("Payment exception")), 
                 "*exception*Reference 2"));

【讨论】:

【参考方案14】:

@Mahmoud Hanafy

我更新了您的答案以适应当前状态。

static class MockLogHelper

    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
        //return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    
        return x => x.Log(level, 0, It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>());
        //return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    
        mock.Verify(Verify<T>(level), times);
    

【讨论】:

【参考方案15】:

使用Telerik Just Mock 创建记录器的模拟实例:

using Telerik.JustMock;
...
context = new XDbContext(Mock.Create<ILogger<XDbContext>>());

【讨论】:

事实上,一个非常愚蠢且没有帮助的评论,主题是关于让 Moq 工作,而不是使用一些商业框架【参考方案16】:

使用 NullLogger - 什么都不做的简约记录器。

public interface ILoggingClass

   public void LogCritical(Exception exception);


public class LoggingClass : ILoggingClass

    private readonly ILogger<LoggingClass> logger;

    public LoggingClass(ILogger<LoggingClass> logger) =>
            this.logger = logger;

    public void LogCritical(Exception exception) =>
        this.logger.LogCritical(exception, exception.Message);

并在测试方法中使用,

ILogger<LoggingClass> logger = new NullLogger<LoggingClass>();
LoggingClass loggingClass = new LoggingClass(logger);

并将 loggingClass 传递给服务进行测试。

【讨论】:

以上是关于如何在 ASP.NET Core 中使用 ILogger 进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 EF Core 在 ASP.NET Core 中取消应用迁移

如何在 ASP.NET Core 中使用 SqlClient?

如何在 ASP.NET Core 中使用 Route 特性

如何在 ASP.NET Core 3.1 中使用 Java?

如何在 ASP.NET Core 中使用区域

如何在 ASP.Net Core 中使用 条件中间件