繁体   English   中英

如何对 Serilog 的 LogContext 属性进行单元测试

[英]How to unit test Serilog's LogContext Properties

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

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

由于LogContext是 static,我认为这很简单,但我低估了一些东西。 这就是我现在所处的位置(为简洁起见,省略了一些代码)。 当 rest 运行时,我确实确认了中间件LogContext.PushProperty行。

考试:

...
[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;
    }
}

我还必须对我记录的事件的推送属性进行单元测试。 假设您按如下方式推送您的财产:

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");
    }
}

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

Serilog 的LogContext将状态应用于跟在 ExecutionContext 之后的“逻辑调用上下文”(请参阅此处的精彩文章)。

您在此处看到的违反直觉的结果是由于应用于逻辑调用上下文的“状态”仅适用于进行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的逻辑调用上下文相同(或外部)的逻辑调用上下文中log.Information

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

PS如果你想测试你的代码产生的日志事件,我可以建议TestCorrelator sink

我知道这是一个老问题,但我不得不做同样的事情,我使用了以下解决方案,扩展了@FooBar 的解决方案:

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

[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(/*...*/);
        /*...*/

        var logEventsArray = TestCorrelator.GetLogEventsFromCurrentContext().ToArray(); 

        foreach (var logEvent in logEventsArray)
        {
            // Assert 
            logEvent.Properties.Should().ContainKeys("foo");
            logEvent.Properties.TryGetValue("foo", out LogEventPropertyValue? result).Should().Be(true);
            result?.ToString().Replace("\"", "").Should().Be("bar"); // replace the quote because it's coming like "bar".
        }

        // Assert 
        TestCorrelator.GetLogEventsFromCurrentContext().Should().ContainSingle().Which.MessageTemplate.Text.Should().Be("foobar");
    }
}

在测试中,我检查相关器的属性以包含所需的键,并查看是否可以获得值和实际的字符串值。 请注意,您需要从 PropertyValue 中解开值并删除所有额外的引号以检查实际值。

我不知道这是否是最好的解决方案,但这是我唯一能想到的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM