这是我的控制器:
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
使用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接口确保了这很困难。