这是我的控制器:

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);
    }
}

正如你所看到的,我有两个依赖项,一个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());
    }
}

当前回答

我已经创建了一个包,Moq。ILogger,使测试ILogger扩展更容易。

实际上,您可以使用类似下面这样更接近实际代码的代码。

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

它不仅更容易编写新的测试,而且维护也没有成本。

回购可以在这里找到,也有一个nuget包(Install-Package ILogger.Moq)。

我也在博客上用一个现实生活中的例子解释了这一点。

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

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"));
}

其他回答

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

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<T>。

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

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

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

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

这是一个通常放在静态helper类中的helper扩展方法:

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());

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

更新。net 6与Moq 4.17.2 此扩展方法还允许使用正则表达式验证消息

static class MockHelper
{
    public static void VerifyLog<T>(this Mock<ILogger<T>> logger, LogLevel level, Times times, string? regex = null) =>
        logger.Verify(m => m.Log(
        level,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((x, y) => regex == null || Regex.IsMatch(x.ToString(), regex)),
        It.IsAny<Exception?>(),
        It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
        times);
}

这就是如何使用它

logger.VerifyLog(LogLevel.Warning, Times.Exactly(2), "Simple match");
logger.VerifyLog(LogLevel.Warning, Times.Exactly(2), "[Yy]ou\scould do regex too.*");

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

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

在@ivan-samygin和@stakx的基础上进一步构建,这里有一些扩展方法,它们也可以匹配Exception和所有日志值(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));
}

更新(感谢@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 < 4.15.0):

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

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

由于在ILogger.Log中的TState曾经是对象,现在是FormattedLogValues问题中描述的变化,不再工作

幸运的是,stakx提供了一个很好的解决方案。所以我把它贴出来,希望能帮别人节省时间(花了一段时间才弄明白):

 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);