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


128

这是我的控制器:

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

如您所见,我有2个依赖项,a IDAO和aILogger

这是我的测试类,我使用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());
    }
}

1
如Ilya所建议的,如果您实际上并没有尝试测试记录方法本身是否被调用,则可以将模拟用作存根。如果是这种情况,模拟记录器将不起作用,您可以尝试几种不同的方法。我写了一篇简短的文章,展示了各种方法。本文包括完整的GitHub存储库,其中包含每个不同的选项。最后,我建议您使用自己的适配器,而不是直接使用ILogger <T>类型,如果您需要的话
ssmith

正如@ssmith所述,在验证实际调用时会遇到一些麻烦ILogger。他在博客中提出了很好的建议,而我的解决方案似乎解决了以下答案中的大多数问题。
Ilya Chernomordik

Answers:


139

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

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

5
要登录到调试输出窗口,请在工厂上调用AddDebug():var factory = serviceProvider.GetService <ILoggerFactory>()。AddDebug();
spottedmahn '17

3
我发现“真实记录器”方法更有效!
DanielV

1
真正的记录器部分还非常适合在特定情况下测试LogConfiguration和LogLevel。
Martin Lottering

这种方法将只允许存根,而不能验证呼叫。我提供了我的解决方案,该解决方案似乎可以解决以下答案中的大多数问题
Ilya Chernomordik

100

实际上,我发现Microsoft.Extensions.Logging.Abstractions.NullLogger<>这看起来是一个完美的解决方案。安装软件包Microsoft.Extensions.Logging.Abstractions,然后按照示例进行配置和使用:

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

这似乎仅适用于.NET Core 2.0,而不适用于.NET Core 1.1。
ThorkilVærge,18年

3
@adospace,您的评论比答案有用得多
johnny

您能否举一个例子说明这将如何工作?进行单元测试时,我希望日志显示在输出窗口中,但不确定是否这样做。
J86

@adospace是否应该在startup.cs中使用?
raklos

1
@raklos哼,不,它应该在实例化ServiceCollection的测试中的启动方法中使用
adospace

31

使用自定义记录器,该记录器使用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()
    {
    }
}

在您的单元测试中使用它,例如

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
  }
}

1
你好 这项工作对我来说很好。现在如何检查或查看我的日志信息
malik saifullah 18/12/8

我直接从VS运行单元测试用例。我没有专用的控制台
malik saifullah 18/12/8

1
@maliksaifullah我正在使用resharper。让我与vs
一起

1
@maliksaifullah VS的TestExplorer提供了打开测试输出的链接。在TestExplorer中选择您的测试,在底部有一个链接
Jehof

1
太好了,谢谢!一些建议:1)不需要通用,因为不使用type参数。实施just ILogger将使它更广泛地可用。2)BeginScope不应返回自身,因为这意味着在运行期间开始和结束作用域的任何经过测试的方法都将处置记录器。而是创建一个私有的“虚拟”嵌套类,该类实现IDisposable并返回该实例的一个实例(然后IDisposable从中删除XunitLogger)。
Tobias J

27

对于使用Moq的.net core 3答案

幸运的是,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);

您保存了我的一天。谢谢。
KiddoDeveloper

15

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

然后,您可以像这样使用它:

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

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


这是一个非常优雅的解决方案。
MichaelDotKnox

我同意,这个答案非常好。我不明白为什么投票数不多
-Farzad

1
晶圆厂 这是非通用的版本ILoggergist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
Tim Abell

是否可以创建模拟来检查我们在LogWarning中传递的字符串?例如:It.Is<string>(s => s.Equals("A parameter is empty!"))
Serhat

这很有帮助。对我来说,缺少的一个部分是如何在写入XUnit输出的模拟上设置回调?从来没有打过我的回调。
flipdoubt

6

正如其他答案建议通过模拟一样ILogger,这很容易,但是突然之间,要验证是否确实对记录器进行了调用就变得更加困难。原因是大多数调用实际上并不属于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%总比没有好:)这种方法将使测试代码

PS对于起订量可以使用写扩展方法的方法了Mock<ILogger<T>>,做Verify来实现类似的结果。

PPS这在.Net Core 3中不再起作用,请检查此线程以获取更多详细信息:https : //github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574


为什么要验证记录器调用?它们不是业务逻辑的一部分。如果发生了一些不好的事情,我宁愿验证程序的实际行为(例如调用错误处理程序或引发异常),也不愿记录消息。
伊利亚·楚马科夫

1
我认为至少在某些情况下进行测试也很重要。我已经看过太多次程序无提示地失败了,所以我认为在异常发生时验证日志记录是否发生是有意义的,例如,它不像“两个”之一,而是测试实际的程序行为和日志记录。
Ilya Chernomordik

5

已经提到您可以像其他任何接口一样模拟它。

var logger = new Mock<ILogger<QueuedHostedService>>();

到目前为止,一切都很好。

不错的是,您可以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>>()));

使用Verify该点时,是针对接口的实际Log方法ILooger而不是扩展方法。


5

这是在@ ivan-samygin和@stakx的工作基础上进一步发展的方法,这些扩展方法也可以在Exception和所有日志值(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));
}


1

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


1
我同意这种观点,正如您所说,构建表达方式可能会有些困难。我经常遇到相同的问题,因此最近将Moq.Contrib.ExpressionBuilders.Logging组合在一起,以提供一个流畅的界面,这使其更加可口。
rgvlee

1

如果仍然是实际的。在.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();
}


0

我曾尝试使用NSubstitute模拟Logger接口(并失败,因为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;
            }
        }
    }

您可以轻松访问所有记录的消息并声明其随附的所有有意义的参数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.