tag net mvc asp c# unit-testing asp.net-core .net-core moq ilogger

c# - mvc - Cómo realizar pruebas unitarias con ILogger en ASP.NET Core



render partial asp net core (7)

Agregando mis 2 centavos, este es un método de extensión auxiliar que generalmente se coloca en una clase auxiliar estática:

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

Entonces, lo usas así:

//Arrange var logger = new Mock<ILogger<YourClass>>(); logger.MockLog(LogLevel.Warning) //Act //Assert logger.Verify(LogLevel.Warning, Times.Once());

Y, por supuesto, puede extenderlo fácilmente para burlarse de cualquier expectativa (es decir, expectativa, mensaje, etc.)

Este es mi controlador:

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

Como puede ver, tengo 2 dependencias, una IDAO y una ILogger

Y esta es mi clase de prueba, uso xUnit para probar y Moq para crear simulacros y trozos, puedo burlarme de DAO fácilmente, pero con el ILogger no sé qué hacer, así que paso nulo y comento la llamada para iniciar sesión en el controlador cuando se ejecuta la prueba. ¿Hay alguna forma de probar pero aún mantener el registrador de alguna manera?

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


En realidad, he encontrado Microsoft.Extensions.Logging.Abstractions.NullLogger<> que parece una solución perfecta.


Es fácil ya que otras respuestas sugieren pasar ILogger simulado, pero de repente se vuelve mucho más problemático verificar que las llamadas realmente se hicieron al registrador. La razón es que la mayoría de las llamadas en realidad no pertenecen a la interfaz ILogger .

Entonces, la mayoría de las llamadas son métodos de extensión que llaman al único método de Log de la interfaz. La razón por la que parece es que es mucho más fácil implementar la interfaz si solo tiene una y no muchas sobrecargas que se reducen al mismo método.

El inconveniente es, por supuesto, que de repente es mucho más difícil verificar que se haya realizado una llamada, ya que la llamada que debe verificar es muy diferente de la llamada que realizó. Hay algunos enfoques diferentes para solucionar este problema, y ​​he descubierto que los métodos de extensión personalizados para simular el marco facilitarán la escritura.

Aquí hay un ejemplo de un método que hice para trabajar con 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>>()); } }

Y así es como se puede usar:

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

Se ve exactamente como si usara el método directamente, el truco aquí es que nuestro método de extensión tiene prioridad porque está "más cerca" en los espacios de nombres que el original, por lo que se usará en su lugar.

Desafortunadamente, no da el 100% de lo que queremos, es decir, los mensajes de error no serán tan buenos, ya que no verificamos directamente en una cadena sino en una lambda que involucra la cadena, pero el 95% es mejor que nada :) Además este enfoque hará que el código de prueba

PD: Para Moq, se puede utilizar el método de escribir un método de extensión para el Mock<ILogger<T>> que Verify para lograr resultados similares.


Solo búrlate así como cualquier otra dependencia:

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

Probablemente necesitará instalar Microsoft.Extensions.Logging.Abstractions paquete Microsoft.Extensions.Logging.Abstractions para usar ILogger<T> .

Además, puede crear un registrador real:

var serviceProvider = new ServiceCollection() .AddLogging() .BuildServiceProvider(); var factory = serviceProvider.GetService<ILoggerFactory>(); var logger = factory.CreateLogger<BlogController>();


Use un registrador personalizado que use ITestOutputHelper (de xunit) para capturar resultados y registros. La siguiente es una pequeña muestra que solo escribe el state en la salida.

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

Úselo en sus pruebas de unidad como

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



Ya lo mencioné, puede burlarse de él como cualquier otra interfaz.

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

Hasta ahora tan bueno.

Lo bueno es que puede usar Moq para verificar que se hayan realizado ciertas llamadas . Por ejemplo, aquí verifico que el registro se haya llamado con una Exception particular.

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

Cuando se utiliza Verify el punto es hacerlo contra el método Log real desde la interfaz ILooger y no con los métodos de extensión.