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

时间:2019-07-26 21:23:34

标签: c# unit-testing serilog

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

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

由于LogContext是静态的,所以我认为这很简单,但是我低估了一些东西。这就是我现在所在的位置(为简洁起见,省略了一些代码)。我确实确认在其余程序运行时中间件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;
    }
}

2 个答案:

答案 0 :(得分:1)

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

environment.js

您可以像下面的示例一样使用Serilog.Sinks.TestCorrelator作为测试专用的Serilog接收器进行单元测试(我也在这里也使用NUnitFluentAssertion

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

答案 1 :(得分:0)

我认为您的单元测试正在使用此处的代码捕获真正的错误。

Serilog的LogContext将状态应用于ExecutionContext后面的“逻辑调用上下文”(请参阅​​出色的文章here)。

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

如果您看一个更简单的示例来演示相同的问题,而不必担心异步/等待任务继续的工作,那么可能会更清楚。

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的逻辑调用上下文相同(或外部)的逻辑调用上下文中推送属性。

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

P.S。如果您想测试代码产生的日志事件,建议使用TestCorrelator sink