NSubstitute ILogger .NET Core
NSubstitute ILogger .NET Core
我正在尝试围绕我的异常处理编写单元测试,以便我可以验证我的记录器是否正确记录了异常。我正在使用 NSubstitute 作为模拟框架,Microsoft.Extensions.Logging.ILogger
我必须遵循我的测试:
[Fact]
public void LogsExcpetionWhenErrorOccursInCreate()
{
var newUser = new UserDataModel
{
FirstName = "Rick",
MiddleName = "Jason",
LastName = "Grimes",
Email = "rick.grimes@thedead.com",
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);
}
}
使用以下方法测试日志记录:
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);
}
}
我的测试失败并出现以下错误:
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>)
我不确定为什么会失败,因为即使在当时的错误消息中,调用也是相同的。
提前致谢!
更新:
这是测试的构造函数,这是我注入记录器模拟的地方:
public UserCollectionTest()
{
_mongoContext = Substitute.For<IMongoContext<UserDataModel>>();
_logger = Substitute.For<ILogger>();
// create UserCollection with our mock client
_collection = new UserCollection(_mongoContext, _logger);
}
LogError 不是 ILogger 方法,因此当您尝试检查是否使用某些参数调用此方法时,NSubstitute 会尝试以某种方式处理它(我不知道具体是如何处理的)但失败了。
LogError扩展方法代码为:
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);
}
所以你必须检查是否调用了 Log 方法。
我稍微简化了您的示例。我觉得思路应该清楚了。
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>>());
}
}
看起来已接受的答案在 .NET Core 3 或 .NET 5 中不起作用。
这是在 github issue
中找到的解决方法
创建一个新的 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);
}
用新的模拟记录器替换模拟记录器
// Old
// var logger = Substitute.For<ILogger>();
// New
var logger = Substitute.For<MockLogger>();
使用新的记录器检查通话
logger.Received().Log(LogLevel.Error, Arg.Is<string>(s => s.Contains("some log message")));
要与通用 ILogger<T>
一起使用,只需将 class 更改为
public abstract class MockLogger<T> : ILogger<T>
这是我的方法,它更进一步。并扩展 Githhub issue.
第一个记录器:
using Microsoft.Extensions.Logging;
public abstract class MockLogger<T> : ILogger<T>
{
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var unboxed = (IReadOnlyList<KeyValuePair<string, object>>)state!;
string message = formatter(state, exception);
this.Log();
this.Log(logLevel, message, exception);
this.Log(logLevel, unboxed.ToDictionary(k => k.Key, v => v.Value), exception);
}
public abstract void Log();
public abstract void Log(LogLevel logLevel, string message, Exception? exception = null);
public abstract void Log(LogLevel logLevel, IDictionary<string, object> state, Exception? exception = null);
public virtual bool IsEnabled(LogLevel logLevel)
{
return true;
}
public abstract IDisposable BeginScope<TState>(TState state);
}
现在的用法:
MockLogger<Service> logger = Substitute.For<MockLogger<Service>>();
logger.IsEnabled(LogLevel.Error).Returns(true);
string originalFormat = "Service returned error with code: '{statusCode}' and reason: '{reason}'";
var statusCode = HttpStatusCode.BadRequest;
string reason = "The reason...";
string message = $"Service returned error with code: '{(int)statusCode}' and reason: '{reason}'";
// Assert
logger.Received(1).Log(
LogLevel.Error,
Arg.Is<IDictionary<string, object>>(
dict =>
dict.Any(kv => kv.Key == "{OriginalFormat}" && (string)kv.Value == originalFormat) &&
dict.Any(kv => kv.Key == "statusCode" && (int)kv.Value == (int)statusCode) &&
dict.Any(kv => kv.Key == "reason" && (string)kv.Value == reason)));
logger.Received(1).Log(LogLevel.Error, message);
logger.Received(1).Log();
它允许单独测试日志参数。
我正在尝试围绕我的异常处理编写单元测试,以便我可以验证我的记录器是否正确记录了异常。我正在使用 NSubstitute 作为模拟框架,Microsoft.Extensions.Logging.ILogger
我必须遵循我的测试:
[Fact]
public void LogsExcpetionWhenErrorOccursInCreate()
{
var newUser = new UserDataModel
{
FirstName = "Rick",
MiddleName = "Jason",
LastName = "Grimes",
Email = "rick.grimes@thedead.com",
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);
}
}
使用以下方法测试日志记录:
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);
}
}
我的测试失败并出现以下错误:
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>)
我不确定为什么会失败,因为即使在当时的错误消息中,调用也是相同的。
提前致谢!
更新:
这是测试的构造函数,这是我注入记录器模拟的地方:
public UserCollectionTest()
{
_mongoContext = Substitute.For<IMongoContext<UserDataModel>>();
_logger = Substitute.For<ILogger>();
// create UserCollection with our mock client
_collection = new UserCollection(_mongoContext, _logger);
}
LogError 不是 ILogger 方法,因此当您尝试检查是否使用某些参数调用此方法时,NSubstitute 会尝试以某种方式处理它(我不知道具体是如何处理的)但失败了。
LogError扩展方法代码为:
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);
}
所以你必须检查是否调用了 Log 方法。
我稍微简化了您的示例。我觉得思路应该清楚了。
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>>());
}
}
看起来已接受的答案在 .NET Core 3 或 .NET 5 中不起作用。
这是在 github issue
中找到的解决方法创建一个新的 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);
}
用新的模拟记录器替换模拟记录器
// Old
// var logger = Substitute.For<ILogger>();
// New
var logger = Substitute.For<MockLogger>();
使用新的记录器检查通话
logger.Received().Log(LogLevel.Error, Arg.Is<string>(s => s.Contains("some log message")));
要与通用 ILogger<T>
一起使用,只需将 class 更改为
public abstract class MockLogger<T> : ILogger<T>
这是我的方法,它更进一步。并扩展 Githhub issue.
第一个记录器:
using Microsoft.Extensions.Logging;
public abstract class MockLogger<T> : ILogger<T>
{
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var unboxed = (IReadOnlyList<KeyValuePair<string, object>>)state!;
string message = formatter(state, exception);
this.Log();
this.Log(logLevel, message, exception);
this.Log(logLevel, unboxed.ToDictionary(k => k.Key, v => v.Value), exception);
}
public abstract void Log();
public abstract void Log(LogLevel logLevel, string message, Exception? exception = null);
public abstract void Log(LogLevel logLevel, IDictionary<string, object> state, Exception? exception = null);
public virtual bool IsEnabled(LogLevel logLevel)
{
return true;
}
public abstract IDisposable BeginScope<TState>(TState state);
}
现在的用法:
MockLogger<Service> logger = Substitute.For<MockLogger<Service>>();
logger.IsEnabled(LogLevel.Error).Returns(true);
string originalFormat = "Service returned error with code: '{statusCode}' and reason: '{reason}'";
var statusCode = HttpStatusCode.BadRequest;
string reason = "The reason...";
string message = $"Service returned error with code: '{(int)statusCode}' and reason: '{reason}'";
// Assert
logger.Received(1).Log(
LogLevel.Error,
Arg.Is<IDictionary<string, object>>(
dict =>
dict.Any(kv => kv.Key == "{OriginalFormat}" && (string)kv.Value == originalFormat) &&
dict.Any(kv => kv.Key == "statusCode" && (int)kv.Value == (int)statusCode) &&
dict.Any(kv => kv.Key == "reason" && (string)kv.Value == reason)));
logger.Received(1).Log(LogLevel.Error, message);
logger.Received(1).Log();
它允许单独测试日志参数。