【问题标题】:How to unit test Serilog's LogContext Properties如何对 Serilog 的 LogContext 属性进行单元测试
【发布时间】:2019-12-05 05:16:25
【问题描述】:

我们有一个相当简单的 netstandard2.0 自定义中间件项目,它使用 Serilog 的静态 LogContext 将指定的 HttpContext 标头复制到日志上下文。

我正在尝试编写一个单元测试,其中我设置了一个使用DelegatingSink 写入变量的记录器。然后它执行Invoke() 中间件方法。然后,我尝试使用该事件来断言已添加属性。到目前为止,中间件添加的属性没有显示,但我在测试中添加的属性会显示。我假设它正在处理不同的上下文,但我不确定如何解决这个问题。我尝试了几种不同的方法,但都没有奏效。

由于LogContext 是静态的,我认为这很简单,但我低估了一些东西。这就是我现在所处的位置(为简洁起见,省略了一些代码)。我确实确认了中间件的 LogContext.PushPropertyis 在其余部分运行时被击中。

测试:

...
[Fact]
public async Task Adds_WidgetId_To_LogContext()
{
    LogEvent lastEvent = null;

    var log = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .WriteTo.Sink(new DelegatingSink(e => lastEvent = e))
        .CreateLogger();
         // tried with and without this, also tried the middleware class name
        //.ForContext<HttpContextCorrelationHeadersLoggingMiddlewareTests>(); 

    var context = await GetInvokedContext().ConfigureAwait(false);

    LogContext.PushProperty("MyTestProperty", "my-value");

    log.Information("test");

    // At this point, 'lastEvent' only has the property "MyTestProperty" :(
}

private async Task<DefaultHttpContext> GetInvokedContext(bool withHeaders = true)
{
    RequestDelegate next = async (innerContext) =>
        await innerContext.Response
            .WriteAsync("Test response.")
            .ConfigureAwait(false);

    var middleware = new MyCustomMiddleware(next, _options);

    var context = new DefaultHttpContext();

    if (withHeaders)
    {
        context.Request.Headers.Add(_options.WidgetIdKey, _widgetId);
    }

    await middleware.Invoke(context).ConfigureAwait(false);

    return context;
}

中间件(测试项目引用本项目):

...
public async Task Invoke(HttpContext context)
{
    if (context == null || context.Request.Headers.Count == 0) { await _next(context).ConfigureAwait(false); }

    var headers = context.Request.Headers;

    foreach (var keyName in KeyNames)
    {
        if (headers.ContainsKey(keyName))
        {
            LogContext.PushProperty(keyName, headers[keyName]);
        }
    }

    await _next(context).ConfigureAwait(false);
}
...

这是我从 Serilog 测试源中窃取的委托接收器:

public class DelegatingSink : ILogEventSink
{
    readonly Action<LogEvent> _write;

    public DelegatingSink(Action<LogEvent> write)
    {
        _write = write ?? throw new ArgumentNullException(nameof(write));
    }

    public void Emit(LogEvent logEvent)
    {
        _write(logEvent);
    }

    public static LogEvent GetLogEvent(Action<ILogger> writeAction)
    {
        LogEvent result = null;

        var l = new LoggerConfiguration()
            .WriteTo.Sink(new DelegatingSink(le => result = le))
            .CreateLogger();

        writeAction(l);

        return result;
    }
}

【问题讨论】:

    标签: c# unit-testing serilog


    【解决方案1】:

    我还必须对已记录事件的推送属性进行单元测试。假设你正在推动你的财产如下:

    public async Task<T> FooAsync(/*...*/)
    {
         /*...*/
         using (LogContext.PushProperty("foo", "bar"))
         {
             Log.Information("foobar");
         }
         /*...*/
    }
    

    你可以像这个例子一样对它进行单元测试,使用Serilog.Sinks.TestCorrelator 作为专门用于测试的 Serilog 接收器(我也在这里使用 NUnitFluentAssertion):

    [Test]
    public async Task Should_assert_something()
    {
        ///Arrange
        // I had issues with unit test seeing log events from other tests running at the same time so I recreate context in each test now
        using (TestCorrelator.CreateContext())
        using (var logger = new LoggerConfiguration().WriteTo.Sink(new TestCorrelatorSink()).Enrich.FromLogContext().CreateLogger())
        {
            Log.Logger = logger;
            /*...*/
            /// Act
            var xyz = await FooAsync(/*...*/)
            /*...*/
    
            /// Assert 
            TestCorrelator.GetLogEventsFromCurrentContext().Should().ContainSingle().Which.MessageTemplate.Text.Should().Be("foobar");
        }
    }
    

    【讨论】:

    • 但这并不能测试该属性是否正确推送?例如如果你删除了 using 语句,测试仍然会通过吗?
    【解决方案2】:

    我认为您的单元测试在此处的代码中发现了一个真正的错误。

    Serilog 的 LogContext 将状态应用于 ExecutionContext 之后的“逻辑调用上下文”(参见一篇精彩的文章 here)。

    您在这里看到的违反直觉的结果是因为应用于逻辑调用上下文的“状态”仅适用于进行LogContext.PushProperty 调用的上下文。外部上下文由内部上下文继承,但内部上下文中的更改不会影响外部上下文。您的异步方法正在创建额外的上下文(您不知道),当您返回原始上下文时,内部上下文中所做的更改将丢失。

    如果您看一个更简单的示例来演示相同的问题,而不必担心 async/await 任务继续的问题,可能会更清楚。

    void ContextExample()
    {
        LogContext.PushProperty("MyOuterProperty", "Foo"); // Apply this property to all log events *within this logical call context*
    
        await Task.Run(() =>
        {
            LogContext.PushProperty("MyInnerProperty", "Bar"); // Apply this property to all log events *within this logical call context*
    
            log.Information("MyFirstLog"); // This log event will contain both MyOuterProperty and MyInnerProperty
        }); // We leave the inner call context, destroying the changes we made to it with PushProperty
    
        log.Information("MySecondLog"); // This log event will contain only MyOuterProperty
    }
    

    要获得您想要的,您必须将属性推送到与您调用log.Information 的逻辑调用上下文相同(或外部)的逻辑调用上下文中。

    另外,您可能希望在PushProperty 的返回值上调用Dispose。它返回一个IDisposable,以便您可以将逻辑调用上下文恢复到其原始状态。如果不这样做,您可能会看到一些奇怪的行为。

    附:如果您想测试您的代码产生的日志事件,我建议您使用TestCorrelator sink

    【讨论】:

    • 谢谢你。我暂时被转移到另一个项目,所以我还不能测试这个理论。
    • @Kizmar 有什么更新吗?我遇到了非常相似的问题。你找到解决办法了吗?
    • @FooBar 不幸的是没有。我们暂时放弃了它。可能会在某个时候重新访问,但不会有一段时间。
    • @Kizmar 我提供了解决方案
    猜你喜欢
    • 2017-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多