it-swarm.com.ru

Как выполнить модульное тестирование с помощью 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 и stub, я могу легко смоделировать DAO, но с ILogger я не знаю, что делать, поэтому я просто передаю null и комментирую вызов log в контроллере при запуске теста. Есть ли способ проверить, но все равно как-нибудь сохранить регистратор?

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());
    }
}
47
duc

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

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>();
62
Ilya Chumakov

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

47
Amir Shitrit

Используйте собственный регистратор, который использует 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
  }
}
9
Jehof

Уже упоминалось, что вы можете смоделировать его как любой другой интерфейс.

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

1
guillem

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

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

0
Ilya Chernomordik

А при использовании StructureMap/Lamar:

var c = new Container(_ =>
{
    _.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});

Docs:

0
Christoph Lütjen