Search code examples
c#unit-testingserilog

How to unit test Serilog's LogContext Properties


We have a fairly simple netstandard2.0 project for custom middleware that uses Serilog's static LogContext to copy specified HttpContext headers to the log context.

I'm trying to write a unit test where I set up a logger that uses a DelegatingSink to write to a variable. It then executes the Invoke() middleware method. I'm then trying to use the event to assert the properties were added. So far the properties being added by the middleware aren't showing up, but the property I'm adding in the test does. I'm assuming it's dealing with different contexts, but I'm not sure how to fix this. I've tried several different things but none have worked.

Since LogContext is static, I assumed this would be pretty straight forward, but I'm underestimating something. This is where I'm at right now (some code omitted for brevity). I did confirm the LogContext.PushProperty line of the middleware is being hit when the rest is run.

The test:

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

The middleware (test project references this project):

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

This is the delegating sink I stole from the Serilog test source:

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


Solution

  • I also had to unit test the pushed properties of my logged events. Assuming you got are pushing your property as follow :

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

    You can unit test it like this example with Serilog.Sinks.TestCorrelator as a Serilog sink dedicated to tests (also I'm using NUnit and FluentAssertion too here):

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