You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对使用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(推荐)

  1. 先安装NuGet包:Serilog.Sinks.InMemory
  2. 调整测试代码如下:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 17:07:16