Introduction

In a previous post, I explored how the FakeLogger introduced by Microsoft provides a clean, built-in alternative to mocking ILogger<T> in unit tests. Instead of dealing with complex mock setups or verifying invocations manually, FakeLogger captures log messages, scopes, and structured data in memory — making assertions both straightforward and expressive.

In this post, we’ll build on that foundation by looking at how to test logging scopes using FakeLogger. We’ll cover why scopes exist, how to use them in code, and how to verify that your logging includes scoped values.

Why Use Scopes?

A logging scope in .NET provides a way to attach contextual information to all log entries within a block of code. Think of it as a lightweight key-value bag that enriches log messages without repeating the same fields on every log line.

For example, you might add a request ID, correlation ID, or entity identifier into a scope — giving every log statement within that block the same contextual metadata. This makes logs far easier to filter and reason about in distributed systems or when debugging specific requests.

Using FakeLogger with Scopes

Let’s start with a simple method that uses a logging scope:

public partial class DemoService(ILogger<DemoService> logger)
{
    public void LogMe(Guid id)
    {
        using var _ = logger.BeginScope(new Dictionary<string, object> { { "id", id } });

        logger.LogInformation("This should be logged.");
    }
}

Here, the BeginScope call creates a logging context with a single key (id).

When logger.LogInformation executes, the scope will automatically include that contextual data — but in a unit test, how can we confirm that the scope was actually applied?

Testing Scope Usage with FakeLogger

The FakeLogger keeps track of every log record, including message templates, log levels, exceptions, and scopes. Let’s test that our DemoService correctly logs the message and attaches the id to the scope:

public class DemoServiceTests
{
    [Fact]
    public void ScopeUsage()
    {
        // Arrange
        var fakeLogger = new FakeLogger<DemoService>();
        var sut = new DemoService(fakeLogger);
        var id = Guid.NewGuid();

        // Act
        sut.LogMe(id);

        // Assert
        fakeLogger.LatestRecord.Message.Should().Be("This should be logged.");

        var scope = fakeLogger.LatestRecord.Scopes[0] as Dictionary<string, object>;
        scope.Should().NotBeNull();
        scope.Keys.Should().Contain("id");
        scope.Values.Should().Contain(id);
    }
}

In this test:

  • We instantiate a system under test, or SUT, using a FakeLogger<DemoService> in the constructor — no mocks, no setup.
  • The LogMe method is called, generating a single log record.
  • We assert the message content and inspect the recorded Scopes collection.
  • Each scope is captured as an object, and in this case, it’s our Dictionary<string, object> containing the id.

This approach is clean, expressive, and framework-supported, removing the need for custom logger wrappers or fragile mock verification setups.

The downside is you likely need to know the implementation of how a scope has been applied however for this type of context I’m ok with that.

Conclusion

Testing logging has historically been awkward — especially when verifying scope or structured data. With Microsoft’s FakeLogger, it’s now simple to assert that log messages, levels, and scopes are all correctly applied, without resorting to third-party mocking libraries or custom test helpers.

In this post, we looked at:

  • How logging scopes provide useful contextual metadata.
  • How FakeLogger can capture and expose those scopes for testing.
  • A concise example showing real-world usage and assertions.

By embracing FakeLogger, you can focus on testing your application logic and logging behaviour together — clearly, simply, and without boilerplate.


Further Reading: