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.
2 Answers
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 :
- Record events so they can be asserted
- 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();
}
}
...
}
Comments
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
Microsoft.Extensions.Diagnostics.Testing package, which exposes the collected events through various properties..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
ILoggerdoesn'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.the ILogger dependency is mockedthat's athere's your problemsituation. 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.mstestisn't a side-note - it listens toDebugso you should keep using Serilog.Sinks.Debug or whatever you do should still write toDebugand Console