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
}
}
Y cuando se usa StructureMap / Lamar:
var c = new Container(_ =>
{
_.For(typeof(ILogger<>)).Use(typeof(NullLogger<>));
});
Documentos:
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.