这是我的控制器:
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());
}
}
我尝试使用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;
}
}
}
您可以轻松地访问所有已记录的消息并断言它提供的所有有意义的参数。
实际上,我找到了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);
}
前面已经提到过,您可以将其模拟为任何其他接口。
var logger = new Mock<ILogger<QueuedHostedService>>();
到目前为止一切顺利。
好的一点是,您可以使用Moq来验证某些调用是否已经执行。例如,在这里,我检查日志已经被一个特定的异常调用。
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时,重点是针对ILooger接口中的真实Log方法,而不是扩展方法进行验证。
仅仅创建一个虚拟的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接口确保了这很困难。
像其他答案建议的那样,传递模拟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