如何在ASP.NET Core中使用ILogger进行单元测试

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)

  • 要登录到调试输出窗口,请在工厂上调用AddDebug():var factory = serviceProvider.GetService <ILoggerFactory>().AddDebug(); (3认同)
  • 我发现"真正的记录器"方法更有效! (2认同)
  • 从 .net 5 开始,应该在“AddLogging”中调用“AddDebug”,而不是从“ILoggerFactory”调用。`new ServiceCollection().AddLogging(builder =&gt; builder.AddDebug())...` (2认同)
  • 对于“AddDebug”,您需要包含 https://www.nuget.org/packages/Microsoft.Extensions.Logging.Debug/ (2认同)

Ami*_*rit 78

实际上,我发现Microsoft.Extensions.Logging.Abstractions.NullLogger<>它看起来像一个完美的解决方案.

  • 为了扩展这个答案,我会说它就像注册NullLoggerFactory一样简单:`services.AddSingleton <ILoggerFactory,NullLoggerFactory>();`并根据需要创建记录器:`var myLogger = loggerFactory.CreateLogger <...>(); ` (15认同)
  • @adospace,您的评论比答案更有用 (2认同)

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)

  • 这太棒了,谢谢!一些建议:1)这不需要是通用的,因为不使用类型参数。仅实现“ILogger”将使其具有更广泛的用途。2) `BeginScope` 不应该返回自身,因为这意味着在运行期间开始和结束范围的任何测试方法都将释放记录器。相反,创建一个实现“IDisposable”的私有“虚拟”嵌套类并返回该类的实例(然后从“XunitLogger”中删除“IDisposable”)。 (4认同)
  • 你好。这对我来说很好用。现在我如何检查或查看我的日志信息 (2认同)

小智 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)


Mah*_*afy 7

加上我的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)

当然,您可以轻松地扩展它以模拟任何期望(即期望,消息等)。


gui*_*lem 6

已经提到你可以像任何其他界面一样模拟它。

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接口的真实方法而不是扩展方法。


Ily*_*dik 6

其他答案建议通过 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

  • 好吧,我认为至少在某些情况下进行测试也很重要。我已经看到太多次程序无声地失败了,所以我认为在发生异常时验证日志记录是有意义的,例如它不像“要么”,而是测试实际的程序行为和日志记录。 (2认同)

Mat*_*s R 6

在@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)