如何对使用ForContext的Serilog调用进行单元测试
Serilog ForContext 链式调用单元测试问题及解决方案
我按照Serilog的通用指南,用ForContext()方法给日志事件添加自定义属性,但看源码发现ForContext()的实现会返回新的Logger对象:
ILogger ForContext(string propertyName, object? value, bool destructureObjects = false) => new LoggerConfiguration() .MinimumLevel.Is(LevelAlias.Minimum) .WriteTo.Logger(this) .CreateLogger() .ForContext(propertyName, value, destructureObjects) ;
这种设计导致链式调用时,单元测试没法验证每个ForContext的调用以及最终的日志方法。比如下面的业务代码:
public class MyService { ILogger _logger; public MyService(ILogger log) { _logger = log; } public void DoStuff() { _logger .ForContext("One", 1) .ForContext("Two", 2) .Warning("Not happy"); } }
对应的测试代码写出来会失败:
public class MyServiceTest { Mock<ILogger> _loggerMock = new(); MyService sut; public MyServiceTest() { _loggerMock.Setup(x => x.Warning(It.IsAny<string>())).Verifiable(); sut = new MyService(_loggerMock.Object); } public void DoStuffShouldLogWarning() { sut.DoStuff(); _loggerMock.Verify(x => x.Warning(It.IsAny<string>()), Times.Once); } }
测试失败的原因是:实际调用的是ForContext("One", 1)和最终的Write()方法,而不是原Mock对象的Warning()方法。请问怎么才能确保日志的属性、值以及级别不会被随意修改?
解决思路
不要直接Mock ILogger,而是用Serilog官方提供的测试工具来捕获并验证日志事件,这样能更准确地验证日志的内容、级别和自定义属性。
方案1:使用Serilog.Sinks.InMemory(推荐)
- 先安装NuGet包:
Serilog.Sinks.InMemory - 调整测试代码如下:
public class MyServiceTest { private readonly MyService _sut; private readonly ILogger _logger; public MyServiceTest() { // 配置InMemory日志接收器 _logger = new LoggerConfiguration() .WriteTo.InMemory() .CreateLogger(); _sut = new MyService(_logger); } [Fact] public void DoStuffShouldLogWarningWithCorrectProperties() { // 执行业务方法 _sut.DoStuff(); // 获取所有捕获的日志事件 var logEvents = InMemorySink.Instance.LogEvents; // 验证日志数量和级别 Assert.Single(logEvents); Assert.Equal(LogEventLevel.Warning, logEvents[0].Level); // 验证日志消息内容 Assert.Equal("Not happy", logEvents[0].MessageTemplate.Text); // 验证自定义属性的键和值 Assert.True(logEvents[0].Properties.ContainsKey("One")); Assert.Equal(1, (int)logEvents[0].Properties["One"].LiteralValue()); Assert.True(logEvents[0].Properties.ContainsKey("Two")); Assert.Equal(2, (int)logEvents[0].Properties["Two"].LiteralValue()); } }
方案2:正确Mock ILogger(不推荐,维护成本高)
如果一定要用Mock,需要让ForContext返回Mock自身或者链式的Mock对象,但这种方式需要大量配置,容易出错:
public class MyServiceTest { private readonly Mock<ILogger> _loggerMock = new(); private readonly MyService _sut; public MyServiceTest() { // 配置ForContext返回Mock自身,保证链式调用能命中同一个Mock实例 _loggerMock.Setup(x => x.ForContext(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<bool>())) .Returns(_loggerMock.Object); _sut = new MyService(_loggerMock.Object); } [Fact] public void DoStuffShouldLogWarning() { _sut.DoStuff(); // 验证日志方法调用 _loggerMock.Verify(x => x.Warning("Not happy"), Times.Once); // 验证两次ForContext的调用参数 _loggerMock.Verify(x => x.ForContext("One", 1, false), Times.Once); _loggerMock.Verify(x => x.ForContext("Two", 2, false), Times.Once); } }
核心结论
推荐使用Serilog官方的测试Sink(比如InMemory)来验证日志,这种方式完全贴合Serilog的实际运行逻辑,能准确验证日志的所有细节,包括自定义属性、级别、消息内容等,避免Mock ILogger带来的适配问题。
内容的提问来源于stack exchange,提问作者tappetyclick
相关产品推荐
相关产品推荐

