I am trying to write unit tests around my exception handling so that I can verify the my logger is properly logging the exception. I am using NSubstitute as a mocking framework and Microsoft.Extensions.Logging.ILogger
I have to following for my test:
[Fact]
public void LogsExcpetionWhenErrorOccursInCreate()
{
var newUser = new UserDataModel
{
FirstName = "Rick",
MiddleName = "Jason",
LastName = "Grimes",
Email = "[email protected]",
Created = new DateTime(2007, 8, 15)
};
var exception = new Exception("Test Exception");
// configure InsertOne to throw a generic excpetion
_mongoContext.InsertOne(newUser).Returns(x => { throw exception; });
try
{
_collection.Create(newUser);
}
catch
{
// validate that the logger logs the exception as an error
_logger.Received().LogError(exception.Message);
}
}
to test the logging in the following method:
public UserDataModel Create(UserDataModel user)
{
try
{
return MongoContext.InsertOne(user);
}
catch(Exception e)
{
_logger?.LogError(e.Message);
throw new DataAccessException("An error occurred while attempting to create a user.", e);
}
}
My test fails with the following error:
Message: NSubstitute.Exceptions.ReceivedCallsException : Expected to receive a call matching:
Log<Object>(Error, 0, Test Exception, <null>, Func<Object, Exception, String>)
Actually received no matching calls.
Received 1 non-matching call (non-matching arguments indicated with '*' characters):
Log<Object>(Error, 0, *Test Exception*, <null>, Func<Object, Exception, String>)
I'm not sure why this is failing because even in then error message the calls are the same.
Thanks in advance!
Update:
Here is the constructor for the test, this is where I am injecting the logger mock:
public UserCollectionTest()
{
_mongoContext = Substitute.For<IMongoContext<UserDataModel>>();
_logger = Substitute.For<ILogger>();
// create UserCollection with our mock client
_collection = new UserCollection(_mongoContext, _logger);
}
ILoggerFactory is a factory interface that we can use to create instances of the ILogger type and register logging providers. It acts as a wrapper for all the logger providers registered to it and a logger it creates can write to all the logger providers at once.
It is designed as a logging API that developers can use to capture built-in ASP.NET logging as well as for their own custom logging. The logging API supports multiple output providers and is extensible to potentially be able to send your application logging anywhere.
2. 1. ILogger is just an interface. You can use the frameworks' own interfaces, but then your app is "locked in" to those. Meaning, it is a high effort to change logging framework later on.
Looks like the accepted answer does not work in .NET Core 3 or .NET 5.
Here is the workaround found in the github issue
Create a new MockLogger class
public abstract class MockLogger : ILogger
{
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) =>
Log(logLevel, formatter(state, exception));
public abstract void Log(LogLevel logLevel, string message);
public virtual bool IsEnabled(LogLevel logLevel) => true;
public abstract IDisposable BeginScope<TState>(TState state);
}
Replace your mocked logger with the new mock logger
// Old
// var logger = Substitute.For<ILogger>();
// New
var logger = Substitute.For<MockLogger>();
Use the new logger to check calls
logger.Received().Log(LogLevel.Error, Arg.Is<string>(s => s.Contains("some log message")));
To use with a generic ILogger<T>
, simply change the class to
public abstract class MockLogger<T> : ILogger<T>
LogError is not a ILogger method, so when you try to check that this method was called with certain parameters NSubstitute tries to handle it somehow(I do not know how exactly) and fails.
The code of LogError extension method is:
public static void LogError(this ILogger logger, string message, params object[] args)
{
if (logger == null)
throw new ArgumentNullException("logger");
logger.Log<object>(LogLevel.Error, (EventId) 0, (object) new FormattedLogValues(message, args), (Exception) null, LoggerExtensions._messageFormatter);
}
So you have to check that Log method was called.
I simplified your example a bit. I think idea should be clear.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var logger = Substitute.For<ILogger>();
try
{
Create(logger);
}
catch
{
logger.CheckErrorMessage("My Message");
}
}
public string Create(ILogger logger)
{
try
{
throw new Exception("My Message");
}
catch (Exception e)
{
logger?.LogError(e.Message);
throw new Exception("An error occurred while attempting to create a user.", e);
}
}
}
public static class TestExtensions
{
public static void CheckErrorMessage(this ILogger logger, string message)
{
logger.Received().Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString() == message),
null,
Arg.Any<Func<object, Exception, string>>());
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With