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

时间:2017-04-15 08:51:23

标签: c# unit-testing asp.net-core moq ilogger

这是我的控制者:

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个依赖项,IDAOILogger

这是我的测试类,我使用xUnit来测试和Moq来创建mock和stub,我可以轻松地模拟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());
    }
}

15 个答案:

答案 0 :(得分:68)

只是嘲笑它以及任何其他依赖:

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

答案 1 :(得分:54)

实际上,我发现Microsoft.Extensions.Logging.Abstractions.NullLogger<>看起来像是一个完美的解决方案。

答案 2 :(得分:9)

使用使用find -name "*.lock" -exec xargs rm {} \; (来自xunit)的自定义记录器来捕获输出和日志。以下是仅将ITestOutputHelper写入输出的小样本。

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()
    {
    }
}

答案 3 :(得分:5)

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

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

答案 4 :(得分:3)

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

当然,您可以轻松地扩展它以模拟任何期望(例如,期望,消息等)

答案 5 :(得分:2)

这里进一步扩展了@ 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));
}

答案 6 :(得分:1)

在使用StructureMap / Lamar时:

var c = new Container(_ =>
{
    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});

文档:

答案 7 :(得分:1)

已经提到您可以将其模拟为其他任何接口。

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时,重点是针对Log接口中的实际ILooger方法而不是扩展方法。

答案 8 :(得分:1)

其他答案建议传递模拟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%总比没有好:)另外,这种方法将使测试代码

P.S。对于Moq,可以使用为Mock<ILogger<T>>编写Verify的扩展方法的方法来获得相似的结果。

答案 9 :(得分:1)

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

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

_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接口可以确保这很困难。

答案 10 :(得分:1)

如果仍然是实际的。在.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();
}

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

我试图使用NSubstitute模拟Logger接口(但失败,因为Arg.Any<T>()需要一个我无法提供的类型参数),但最终创建了一个测试记录器(类似于@jehof的回答)通过以下方式:

    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 :(得分:0)

我创建了一个程序包 Moq.ILogger ,以简化ILogger扩展的测试。

实际上,您可以使用类似于以下内容的代码。

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

不仅更容易编写新测试,而且维护成本低廉。

可以在here中找到该仓库,并且也有一个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 :(得分:0)

使用 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 传递给服务进行测试。