这是我的控制器:

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

当前回答

使用一个使用ITestOutputHelper(来自xunit)的自定义记录器来捕获输出和日志。下面是一个只将状态写入输出的小示例。

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

其他回答

当使用StructureMap / Lamar时:

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

文档:

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

使用一个使用ITestOutputHelper(来自xunit)的自定义记录器来捕获输出和日志。下面是一个只将状态写入输出的小示例。

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

我已经创建了一个包,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"));
}

实际上,我找到了Microsoft.Extensions.Logging.Abstractions。NullLogger<>,这看起来像是一个完美的解决方案。安装包Microsoft.Extensions.Logging。抽象,然后按照示例配置和使用它:

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

我尝试使用NSubstitute模拟记录器接口(并且失败了,因为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;
            }
        }
    }

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