web-dev-qa-db-fra.com

Comment effectuer des tests unitaires avec ILogger dans ASP.NET Core

Ceci est mon contrôleur:

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

Comme vous pouvez le voir, j'ai 2 dépendances, une IDAO et une ILogger

Et ceci est ma classe de test, j'utilise xUnit pour tester et Moq pour créer une maquette et un moignon, je peux simuler DAO facile, mais avec la ILogger je ne sais pas quoi faire, je passe simplement null et commente l'appel de log dans le contrôleur lors de l'exécution du test. Existe-t-il un moyen de tester mais de conserver l’enregistreur?

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

Il suffit de s'en moquer ainsi que de toute autre dépendance:

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

Vous aurez probablement besoin d'installer le package Microsoft.Extensions.Logging.Abstractions pour utiliser ILogger<T>

De plus, vous pouvez créer un véritable enregistreur:

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();
62
Ilya Chumakov

En fait, j'ai trouvé Microsoft.Extensions.Logging.Abstractions.NullLogger<> qui ressemble à une solution parfaite.

47
Amir Shitrit

Utilisez un enregistreur personnalisé qui utilise ITestOutputHelper (de xunit) pour capturer la sortie et les journaux. Voici un petit exemple qui écrit uniquement la state dans la sortie.

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()
    {
    }
}

Utilisez-le dans vos unittests comme

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

Ajout de mes 2 centimes, c’est une méthode d’extension d’aide généralement placée dans une classe d’aide statique:

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

Ensuite, vous l'utilisez comme ceci:

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

//Act

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

Et bien sûr, vous pouvez facilement l’étendre pour répondre à toutes les attentes (par ex. Expection, message, etc.).

3
Mahmoud Hanafy

Déjà mentionné, vous pouvez vous en moquer comme n'importe quelle autre interface.

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

Jusqu'ici tout va bien.

La bonne chose est que vous pouvez utiliser Moq pour vérifier que certains appels ont été effectués . Par exemple, ici, je vérifie que le journal a été appelé avec une Exception particulière.

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

Lorsque vous utilisez Verify, le point est de le faire par rapport à la méthode réelle Log à partir de l'interface ILooger et non aux méthodes d'extension.

1
guillem

C’est facile comme d’autres réponses suggèrent de passer simulacre ILogger, mais il devient soudainement beaucoup plus problématique de vérifier que les appels ont bien été passés vers l’enregistreur. La raison en est que la plupart des appels n'appartiennent pas à l'interface ILogger elle-même.

Donc, la plupart des appels sont des méthodes d'extension qui appellent la seule méthode Log de l'interface. La raison en est qu’il est beaucoup plus facile de faciliter la mise en oeuvre de l’interface si vous n’avez qu’une seule surcharge et pas beaucoup de surcharges qui se résume à la même méthode.

L’inconvénient est bien sûr qu’il est soudainement beaucoup plus difficile de vérifier qu’un appel a été passé car l’appel que vous devez vérifier est très différent de celui que vous avez passé. Il existe différentes approches pour contourner ce problème, et j’ai constaté que les méthodes d’extension personnalisées pour la structure moqueuse faciliteraient l’écriture.

Voici un exemple de méthode que j'ai créée pour travailler avec 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>>());
    }

}

Et voici comment cela peut être utilisé:

_logger.Received(1).LogError("Something bad happened");   

On dirait exactement que vous avez utilisé la méthode directement, le truc ici est que notre méthode d’extension a la priorité car elle est "plus proche" dans les espaces de noms que celle d'origine, elle sera donc utilisée à la place.

Malheureusement, cela ne donne pas à 100% ce que nous voulons, à savoir que les messages d'erreur ne seront pas aussi bons, car nous ne vérifions pas directement sur une chaîne, mais plutôt sur un lambda qui implique la chaîne, mais 95% est meilleur que rien :) De plus cette approche rendra le code de test 

P.S. Pour Moq, vous pouvez utiliser l’approche consistant à écrire une méthode d’extension pour le Mock<ILogger<T>> qui fait Verify pour obtenir des résultats similaires.

0
Ilya Chernomordik

Et lorsque vous utilisez StructureMap/Lamar:

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

Docs:

0