这是我的控制器:

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

当前回答

像其他答案建议的那样,传递模拟ILogger很容易,但是验证调用实际上是对logger进行的突然变得更加困难。原因是大多数调用实际上并不属于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>>编写扩展方法的方法来验证,以实现类似的结果。

P.P.S.这在。net Core 3中不再工作,查看这个线程了解更多细节:https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574

其他回答

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

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

当使用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/

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

仅仅创建一个虚拟的ILogger对于单元测试来说没有多大价值。您还应该验证是否进行了日志记录调用。您可以使用Moq注入一个模拟ILogger,但验证调用可能有点棘手。本文将深入探讨如何使用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接口确保了这很困难。

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

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