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());
}
}
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>();
En fait, j'ai trouvé Microsoft.Extensions.Logging.Abstractions.NullLogger<>
qui ressemble à une solution parfaite.
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
}
}
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.).
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.
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.
Et lorsque vous utilisez StructureMap/Lamar:
var c = new Container(_ =>
{
_.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});
Docs: