19

I use serilog in my projects with the static logger approach - it's nice and easy to be able to call Log.X on my class libraries rather than injecting a logger class everywhere.

However when it comes to unit/integration testing, if a test has failed it would be hugely beneficial to see the error logs from the class libraries (moreso for integration tests).

Because I am not injecting an ILogger into my classes (due to use of static logger), I can't create a mock test logger that writes output to the test logs.

Has anyone managed to output messages to XUnit using the Serilog global (static) logger?

MarredCheese
  • 17,541
  • 8
  • 92
  • 91
aspirant_sensei
  • 1,568
  • 1
  • 16
  • 36
  • put https://github.com/jet/equinox/blob/4d3375389ad2ca24318a302e8ffd562194cf4342/tests/Equinox.EventStore.Integration/Infrastructure.fs#L35-L52 into `Log.Logger` (Including a `WriteTo.Seq()` can be super useful too) – Ruben Bartelink Nov 18 '19 at 23:15

2 Answers2

30

The Serilog.Sinks.XUnit nuget package makes it easy to accomplish this. Reference the nuget in your project. Then you can use the static logger in the test:

using Serilog;
using Xunit;
using Xunit.Abstractions;

namespace XunitToSerilog
{
    public class SampleTest
    {
        public SampleTest(ITestOutputHelper output)
        {
            Log.Logger = new LoggerConfiguration()
            // add the xunit test output sink to the serilog logger
            // https://github.com/trbenning/serilog-sinks-xunit#serilog-sinks-xunit
            .WriteTo.TestOutput(output)
            .CreateLogger();
        }

        [Fact]
        public void Test1()
        {
            Log.Information("goes to test output");
        }
    }
}
rachri
  • 525
  • 6
  • 13
  • 2
    Here initialization of Serilog+xUnit happens in a test class constructor. So you have to write the same initialization logic in each test class file? Is it possible to do it at program entry point globally? – Bad Jun 24 '22 at 15:18
  • 1
    @Bad The key point here ist the connection of the **ITestOutputHelper output** to the logger. [ITestOutputHelper is injected by XUnit into the class constructor](https://xunit.net/docs/capturing-output). This design decision makes sense so that the test runner can assign the output to a specific Test. You could derive all your test classes from a base class which is doing the initialization so that you write less code. Or maybe you find another way using [Shared Context between Tests](https://xunit.net/docs/shared-context) see CollectionFixture. – rachri Jun 25 '22 at 16:05
  • I think what would actually be nice is connecting the output during dependency injection phase, like in the startup ConfigureServices method. You can do something like loggerFactory.AddProvider and specify the XunitTestOutputLoggerProvider, but I want to actually specify a serilog provider. no idea how to do this. – Mark Lauter May 09 '23 at 16:02
0

If, on the other hand, you want to test the output of the logs, a quick and dirty way might be:

using FluentAssertions;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace TestNamespace;

public class TestClass
{
    private readonly TestOutputHelper _logOutput;

    public TestClass(ITestOutputHelper logOutput)
    {
        _logOutput = (TestOutputHelper)logOutput;

        Log.Logger = new LoggerConfiguration()
            .WriteTo.TestOutput(_logOutput)
            .CreateLogger();
    }

    [Fact]
    public async Task TestMethodShould()
    {
        var foo = "bar";
    
        // method under test should use serilog to Log.Warn(...);
        var result = await classUnderTest.MethodUnderTest(foo);
        
        _logOutput.Output.Should().NotBeNullOrEmpty();
        _logOutput.Output.Trim().Split("\n").Should().HaveCount(1);
        _logOutput.Output.Should().Contain("WRN] log message that should return");
    }
}
Ruskin
  • 5,721
  • 4
  • 45
  • 62