duc*_*duc 81 c# unit-testing moq asp.net-core 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);
}
}
Run Code Online (Sandbox Code Playgroud)
如你所见,我有2个依赖项,a IDAO
和aILogger
这是我的测试类,我使用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());
}
}
Run Code Online (Sandbox Code Playgroud)
Ily*_*kov 97
只是嘲笑它以及任何其他依赖:
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);
Run Code Online (Sandbox Code Playgroud)
您可能需要安装Microsoft.Extensions.Logging.Abstractions
要使用的包ILogger<T>
.
此外,您可以创建一个真正的记录器:
var serviceProvider = new ServiceCollection()
.AddLogging()
.BuildServiceProvider();
var factory = serviceProvider.GetService<ILoggerFactory>();
var logger = factory.CreateLogger<BlogController>();
Run Code Online (Sandbox Code Playgroud)
Ami*_*rit 78
实际上,我发现Microsoft.Extensions.Logging.Abstractions.NullLogger<>
它看起来像一个完美的解决方案.
Iva*_*gin 57
更新(感谢@Gopal Krishnan 的评论):
使用 Moq >= 4.15.0 以下代码正在工作(不再需要演员表):
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>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.Once);
Run Code Online (Sandbox Code Playgroud)
先前版本的答案(对于 Moq < 4.15.0):
对于使用 Moq 的 .net core 3 个答案
由于ILogger.Log 中的问题TState 曾经是对象,现在是 FormattedLogValues,因此不再工作
幸运的是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);
Run Code Online (Sandbox Code Playgroud)
Ton*_*oei 26
最简单的解决方案是使用NullLogger
. 它是 的一部分Microsoft.Extensions.Logging.Abstractions
。
无需搞乱工厂和其他不必要的建筑。只需添加:
ILogger<BlogController> logger = new NullLogger<BlogController>();
Run Code Online (Sandbox Code Playgroud)
Jeh*_*hof 20
使用自定义记录器ITestOutputHelper
(使用xunit)捕获输出和日志.以下是仅将示例写入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()
{
}
}
Run Code Online (Sandbox Code Playgroud)
在你的单元测试中使用它
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
}
}
Run Code Online (Sandbox Code Playgroud)
小智 11
如果还是实际的话。在 .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();
}
Run Code Online (Sandbox Code Playgroud)
加上我的2美分,这是通常在静态helper类中放入的helper扩展方法:
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);
}
}
Run Code Online (Sandbox Code Playgroud)
然后,您可以像这样使用它:
//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)
//Act
//Assert
logger.Verify(LogLevel.Warning, Times.Once());
Run Code Online (Sandbox Code Playgroud)
当然,您可以轻松地扩展它以模拟任何期望(即期望,消息等)。
已经提到你可以像任何其他界面一样模拟它。
var logger = new Mock<ILogger<QueuedHostedService>>();
Run Code Online (Sandbox Code Playgroud)
到现在为止还挺好。
不错的是,您可以使用它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>>()));
Run Code Online (Sandbox Code Playgroud)
使用Verify
重点是针对Log
来自ILooger
接口的真实方法而不是扩展方法。
其他答案建议通过 mock 很容易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>>());
}
}
Run Code Online (Sandbox Code Playgroud)
这就是它的使用方式:
_logger.Received(1).LogError("Something bad happened");
Run Code Online (Sandbox Code Playgroud)
看起来就像您直接使用该方法一样,这里的技巧是我们的扩展方法获得优先级,因为它在命名空间中比原始方法“更接近”,因此将改为使用它。
不幸的是,它没有给出我们想要的 100%,即错误消息不会那么好,因为我们不直接检查字符串,而是检查涉及字符串的 lambda,但 95% 总比没有好:) 另外这种方法将使测试代码
PS对于起订量可以使用写扩展方法的的方法Mock<ILogger<T>>
,做Verify
来实现类似的结果。
PPS 这不再适用于 .Net Core 3,请查看此线程以获取更多详细信息:https : //github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574
在@ivan-samygin 和@stakx 的工作基础上进一步构建,这里有一些扩展方法也可以匹配异常和所有日志值(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));
}
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
41281 次 |
最近记录: |