-1

I need to have every invocation from the ILogger interface of a test, but I mock the ILogger interface in the class that is going to be tested, so it won't write to console and therefore the ILogger logs won't show in the sdtout of the test explorer in Visual Studio.

3
  • Is the real question how to record log events during unit testing? ILogger doesn't write to the console, it writes to whatever target you specify. You almost never need to mock the ILogger interface anyway. Even if your unit test framework doesn't use the Console (eg xUnit) you can create a proper logger sink that writes to xUnit's test output. Commented Nov 19 at 8:09
  • The implementation is with Serilog Sinks to Console,Debug and Seq but in the Test the ILogger dependency is mocked so it doesnt write to Console and therefore isnt seen in the Test-Explorere. Our Tests are with MSTest as a side note Commented Nov 19 at 8:29
  • the ILogger dependency is mocked that's a there's your problem situation. What are you actually trying to do, send the output to the test explorer or record and assert it? Or both? The implementations are different, but LoggerProvider can probably do both. mstest isn't a side-note - it listens to Debug so you should keep using Serilog.Sinks.Debug or whatever you do should still write to Debug and Console Commented Nov 19 at 8:48

2 Answers 2

1

The real requirement is how to implement specific functionality and so mocking isn't the correct approach. It can be done, but it's a lot more complex.

It seems the real requirements are :

  1. Record events so they can be asserted
  2. Keep showing log output in the Test Explorer when using MSTest

The testing framework is important - xUnit for example doesn't record Console/Debug output. MSTest does. To cover #2 all one needs is to keep using a Console or Debug sink.

That means we only need to create a new Serilog sink to record log events and add it to LoggerConfiguration.

A very quick&dirty implementation would be :

public class RecorderSink : ILogEventSink
{
    static ConcurrentQueue<LogEvent> _events=new();

    public static IReadOnliList<LogEvent> GetEvents()=>_events.ToList();

    public void Emit(LogEvent logEvent)
    {
        _events.Enqueue(logEvent);
    }
}

But Mitch Bodmer created something far better - a TestCorrelator sink that records events inside a test, or rather, a specific context.

You can record log events to the TestCorrelator with

Log.Logger = new LoggerConfiguration().WriteTo.TestCorrelator().CreateLogger();

And use it in your unit tests to verify the events generated in a test with :

using (TestCorrelator.CreateContext())
{
    //Pretend this is actual code
    Log.Information("My log message!");

    TestCorrelator.GetLogEventsFromCurrentContext()
        .Should().ContainSingle()
        .Which.MessageTemplate.Text
        .Should().Be("My log message!");
}

The project uses MSTest unit tests so one can see how it can be used in actual unit tests.

With a slight modification, to keep writing to the console :

[TestClass]
public class TestCorrelatorTests
{
    [AssemblyInitialize]
    public static void ConfigureGlobalLogger(TestContext testContext)
    {
        Log.Logger = new LoggerConfiguration()
                        .WriteTo.TestCorrelator()
                        .WriteTo.Console()
                        .CreateLogger();
    }

    [TestMethod]
    public void A_context_captures_all_LogEvents_emitted_to_a_TestCorrelatorContext_within_it()
    {
        using (TestCorrelator.CreateContext())
        {
            Log.Information("");

            TestCorrelator.GetLogEventsFromCurrentContext()
                .Should().ContainSingle();
        }
    }
   ...
}
Sign up to request clarification or add additional context in comments.

Comments

0

My solution after a lot of fiddling around was to add a callback, but couldn't get the parameter matching right until I discovered a thread that does it with a separate delegate.

This for me still unknown reason works as intended, and I hope someone can reuse this.

My test sample is built with a custom builder pattern so this can be ignored the mockLogger is the important thing to look for

internal static class MoqExtensions
{
  internal static Action<LogLevel, EventId, object, Exception?, Delegate> InvokeCallback = (l, _, s, ex, f) =>
  {
    string? message = f.DynamicInvoke(s, ex) as string;
    Console.WriteLine($"[MockLogger] Loglevel: {l.ToString().PadRight(11)} Message: {message}");
  };
}
using Moq;
using FluentAssertions;

[TestClass]
public class SampleTestClass
{
  [TestMethod]
  public async Task SampleTestMethode()
  {
    // prep
    Mock<ILogger<ClassTobeTested>> mockLogger = new Mock<ILogger<ClassTobeTested>>();
    mockLogger.Setup(logger => logger.Log(
        It.IsAny<LogLevel>(),
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((v, t) => true),
        It.IsAny<Exception>(),
        (Func<object, Exception?, string>)It.IsAny<object>()
            ))
      .Callback(MoqExtensions.InvokeCallback);

    PakageData input = new PakageDataBuilder().Build();
    ClassTobeTested proband = new ClassTobeTestedBuilder()
      .WithILogger(mockLogger)
      .Build();

    StandortOptions stdopt = new StandortOptionsBuilder().Build();

    // act
    bool result = await proband.SetOrder(input, stdopt);

    // test
    result.Should().BeTrue();
  }
}

3 Comments

What you ask isn't mocking - you're trying to implement a very specific logger, one that records all events. It would be a lot simpler to just implement the interface. There's also the FakeLogger in the Microsoft.Extensions.Diagnostics.Testing package, which exposes the collected events through various properties.
There are cases where i .Verify the existence of Certain logs this requires to Moq the ILogger interface, but that is not the point.
this requires to Moq no, that's just one way to implement it - and not very good either. Verify doesn't check data, it checks whether specific function calls were made. The actual data and ordering are lost. I already linked to FakeLogger. You can also use LoggerProvider normally with a sink that writes to a List<LogEvent>, keeping both your Console output that's needed by MSTest, and the verifiable list of events

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.