Подтвердить что ты не робот

Как unit test с ILogger в ASP.NET Core

Это мой контроллер:

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 зависимости: IDAO и ILogger

И это мой тестовый класс, я использую xUnit для тестирования и Moq для создания макета и заглушки, я могу mock 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());
    }
}
4b9b3361

Ответ 1

Просто издевайтесь над ним, а также с любой другой зависимостью:

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

Ответ 2

На самом деле я нашел Microsoft.Extensions.Logging.Abstractions.NullLogger<> который выглядит как идеальное решение.

Ответ 3

Используйте специальный журнал, который использует 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()
    {
    }
}

Используйте его в своих unittests, например

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

Ответ 4

Вы можете использовать макет как заглушку, как предполагает Илья, если вы на самом деле не пытаетесь проверить, что сам метод ведения журнала был вызван. Если это так, то издевательство над журналом не работает, и вы можете попробовать несколько разных подходов.

Я написал короткую статью демонстрирующую множество подходов. В статье содержится полное репо GitHub с каждым из разных вариантов. В конце концов, моя рекомендация состоит в том, чтобы использовать ваш собственный адаптер, а не работать напрямую с типом ILogger, если вам нужно проверить, что он вызывается.

https://ardalis.com/testing-logging-in-aspnet-core

Ответ 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 а не с методами расширения.

Ответ 7

Это легко, так как другие ответы предлагают передать ложный ILogger, но внезапно становится намного более проблематичным проверить, действительно ли звонки были сделаны регистратору. Причина в том, что большинство вызовов на самом деле не принадлежат 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% того, что мы хотим, а именно сообщения об ошибках не будут такими хорошими, так как мы не проверяем непосредственно строку, а скорее лямбду, которая включает строку, но 95% лучше, чем ничего :) Дополнительно этот подход сделает тестовый код

PS Для Moq можно использовать подход написания метода расширения для Mock<ILogger<T>> который делает Verify для достижения аналогичных результатов.

Ответ 8

Добавляя мои 2 цента, это вспомогательный метод расширения, обычно помещаемый в статический вспомогательный класс:

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

И, конечно, вы можете легко расширить его, чтобы высмеивать любые ожидания (например, ожидание, сообщение и т.д.)