Reputation: 4506
Until now, we I was mocking the ILogger.LogXXX
calls by following this approach.
Unfortunately, after updating the project to .net core 3.0, and if you're using strict mocks (Moq), it will always complain about not having a corresponding setup:
Moq.MockException : ILogger.Log<FormattedLogValues>(LogLevel.Information, 0,
Inicio de cancelamento de reserva: Grm.GestaoFrotas.Dtos.Reservas.Mensagens.MsgCancelamentoReserva,
null,
Func<FormattedLogValues, Exception, string>) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.
Unfortunately, I can't simple change object with FormattedLogValues
like this:
_container.GetMock<ILogger<GestorReservas>>()
.Setup(l => l.Log(It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<FormattedLogValues>(),
It.IsAny<Exception>(),
It.IsAny<Func<FormattedLogValues, Exception, string>()));
This won't work because FormattedLogValues
is internal.
I can always change the mocking strategy (strict to loose), but I'd prefer to keep it as it is (strict). So, any clues on how to solve this?
Thanks.
Upvotes: 6
Views: 2059
Reputation: 27842
This expands on Muk's answer.
My addition is "ILoggerFactory"..and how you "code up" the actually logging inside your real class.
First, unit test code:
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
private Mock<ILoggerFactory> GetDefaultILoggerFactoryMock()
{
Mock<ILoggerFactory> returnMock = new Mock<ILoggerFactory>(MockBehavior.Loose);
returnMock.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(
() =>
this.GetDefaultILoggerMock<MyConcreteClassThatUsesILoggerFactoryInItsConstructor>().Object);
return returnMock;
}
private Mock<ILogger<T>> GetDefaultILoggerMock<T>()
{
Mock<ILogger<T>> returnMock = new Mock<ILogger<T>>(MockBehavior.Strict);
returnMock.Setup(
m => m.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<object>(),
It.IsAny<Exception>(),
It.IsAny<Func<object, Exception, string>>())).Callback(
(LogLevel ll, EventId eid, object obj1, Exception ex, Func<object, Exception, string> func) =>
{
Console.WriteLine(func.Invoke(obj1, ex));
}).Verifiable();
returnMock.Setup(m => m.IsEnabled(It.IsAny<LogLevel>())).Returns(false);
return returnMock;
}
Now, in your actual class (here I call it 'MyConcreteClassThatUsesILoggerFactoryInItsConstructor')..you need to use the NON EXTENSION METHOD of making log calls. (All the helper methods like LogInformation are static extension methods overloads)
So like this: (I am using very "wordy" values to clarify what is what (a log-msg/"state" vs what the formatter does). (Your code probably won't be "wordy" like mine).
string logMsgAkaTheState = "Here_Is_The_Log_Message_Aka_The_StateArgument";
Func<object, Exception, string> formatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message) + "*formatter-end*";
logger.Log(LogLevel.Information, ushort.MaxValue, logMsgAkaTheState, null, formatterFunc);
or for one that is an exception
catch (Exception ex)
{
string logMsgAkaTheState = "Here_Is_Another_Log_Message_Aka_The_StateArgument";
Func<object, Exception, string> errorFormatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message) + "*formatter-end*";
this.logger.Log(LogLevel.Error, ushort.MaxValue, null, ex, errorFormatterFunc);
}
If you do both of those things, especially the second part, you can Mock the ILoggerFactory / ILogger
Here is the germane parts of the class/constructor:
using Microsoft.Extensions.Logging;
public class MyConcreteClassThatUsesILoggerFactoryInItsConstructor : IMyConcreteClassThatUsesILoggerFactoryInItsConstructor
{
public const string ErrorMsgILoggerFactoryIsNull = "ILoggerFactory is null";
private readonly ILogger<MyConcreteClassThatUsesILoggerFactoryInItsConstructor> logger;
public MyConcreteClassThatUsesILoggerFactoryInItsConstructor(
ILoggerFactory loggerFactory)
{
if (null == loggerFactory)
{
throw new ArgumentNullException(ErrorMsgILoggerFactoryIsNull, (Exception)null);
}
this.logger = loggerFactory.CreateLogger<MyConcreteClassThatUsesILoggerFactoryInItsConstructor>();
}
public void DoSomething()
{
string logMsgAkaTheState = "Here_Is_The_Log_Message_Aka_The_StateArgument";
Func<object, Exception, string> formatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message) + "*formatter-end*";
logger.Log(LogLevel.Information, ushort.MaxValue, logMsgAkaTheState, null, formatterFunc);
int div = 0;
try
{
int x = 1 / div;
}
catch (Exception ex)
{
string logMsgAkaTheState = "Here_Is_Another_The_Log_Message_Aka_The_StateArgument";
Func<object, Exception, string> errorFormatterFunc = (theObj, theException) => "*formatter-start*" + theObj + "*formatter-middle*" + (null == theException ? "theExceptionIsNull" : theException.Message) + "*formatter-end*";
this.logger.Log(LogLevel.Error, ushort.MaxValue, null, ex, errorFormatterFunc);
}
}
}
Upvotes: -1
Reputation: 363
https://adamstorr.azurewebsites.net/blog/mocking-ilogger-with-moq describes it with a Moq only solution.
loggerMock
.Verify(l => l.Log(
It.Is<LogLevel>(l => l == LogLevel.Information),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString() == "Message"),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.AtLeastOnce);
Upvotes: -2
Reputation: 71
Try this:
// <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
// <PackageReference Include="Moq" Version="4.13.1" />
container.GetMock<ILogger<GestorReservas>>()
.Setup(l => l.Log(It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>())));
Upvotes: 6