
Unit Testing Anti-Pattern: Working with Logger
Testing
7 Articles
Table of Contents
What we gonna do?
Testing logging functionality can be challenging, as it is not always clear whether it should be tested or how to approach it. This discussion explores the key questions: Should you test logging? How should it be tested? How much logging is necessary? And how should logger instances be managed?
Why we gonna do?
Logging is a cross-cutting concern present in various parts of an application. While it provides valuable insights into the application's behavior, it may also introduce unnecessary complexity if overused. Understanding whether logging is a business requirement or merely an implementation detail helps determine whether it should be tested. If logs are meant to be consumed by users or stakeholders, they should be tested. If they are for developers only, they might not require formal testing.
Excessive or misplaced logging can clutter the codebase and reduce maintainability . Proper testing of logging ensures that logs serve their intended purpose—whether for business tracking or development debugging—without introducing unnecessary complexity.
How we gonna do?
Example: Implementing Domain Event-Based Logging in C#
1. Define a Domain Event
The event represents a significant domain occurrence that needs to be logged.
public class OrderPlacedEvent : IDomainEvent
{
public Guid OrderId { get; }
public DateTime OccurredOn { get; }
public OrderPlacedEvent(Guid orderId)
{
OrderId = orderId;
OccurredOn = DateTime.UtcNow;
}
}
2. Implement a Domain Event Handler for Logging
The handler listens to domain events and logs relevant details.
public class OrderPlacedEventHandler : IDomainEventHandler<OrderPlacedEvent>
{
private readonly ILogger<OrderPlacedEventHandler> _logger;
public OrderPlacedEventHandler(ILogger<OrderPlacedEventHandler> logger)
{
_logger = logger;
}
public Task Handle(OrderPlacedEvent domainEvent, CancellationToken cancellationToken)
{
_logger.LogInformation("Order placed: {OrderId} at {OccurredOn}", domainEvent.OrderId, domainEvent.OccurredOn);
return Task.CompletedTask;
}
}
3. Event Dispatcher (Publishing the Event)
The event is dispatched when the order is placed.
public class OrderService
{
private readonly IDomainEventDispatcher _eventDispatcher;
public OrderService(IDomainEventDispatcher eventDispatcher)
{
_eventDispatcher = eventDispatcher;
}
public async Task PlaceOrder(Guid orderId)
{
// Business logic for placing an order
var orderPlacedEvent = new OrderPlacedEvent(orderId);
await _eventDispatcher.Dispatch(orderPlacedEvent);
}
}
4. Domain Event Dispatcher
A generic dispatcher to ensure all domain events are handled.
public interface IDomainEventDispatcher
{
Task Dispatch<T>(T domainEvent) where T : IDomainEvent;
}
public class DomainEventDispatcher : IDomainEventDispatcher
{
private readonly IEnumerable<IDomainEventHandler<OrderPlacedEvent>> _handlers;
public DomainEventDispatcher(IEnumerable<IDomainEventHandler<OrderPlacedEvent>> handlers)
{
_handlers = handlers;
}
public async Task Dispatch<T>(T domainEvent) where T : IDomainEvent
{
foreach (var handler in _handlers)
{
await handler.Handle((OrderPlacedEvent)(object)domainEvent, CancellationToken.None);
}
}
}
You can also use the FakeLogger introduced in .NET 8. Below is the code sample that shows testing logging using Mock vs FakeLogger .
Before Fake Logger with Mock - Complex to Setup and Verify
[Fact]
public async Task InvokeAsync_ShouldReturnInternalServerError_For_Exception()
{
var logger = new Mock<ILogger<ExceptionHandlerMiddleware>>();
var env = new Mock<IWebHostEnvironment>();
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
RequestDelegate next = (HttpContext httpContext) => throw new Exception("Test");
var middleware = new ExceptionHandlerMiddleware(env.Object, logger.Object);
await middleware.InvokeAsync(context, next);
logger.Verify(
x => x.Log(
LogLevel.Critical,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => string.Equals("Test", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
After Fake Logger - Simpler Setup, Assertion, and Testing
[Fact]
public async Task InvokeAsync_ShouldReturnInternalServerError_For_Exception()
{
var logger = new FakeLogger<ExceptionHandlerMiddleware>();
var env = new Mock<IWebHostEnvironment>();
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
static Task next(HttpContext httpContext) => throw new Exception("Test");
var middleware = new ExceptionHandlerMiddleware(env.Object, logger);
await middleware.InvokeAsync(context, next);
Assert.Equal(1, logger.Collector.Count);
Assert.Equal(LogLevel.Critical, logger.LatestRecord.Level);
Assert.Equal("Test", logger.LatestRecord.Exception?.Message);
}
As you see above, the usage of FakeLogger Simplified testing a lot by reducing complex assertion.
Summary
Testing logging is essential when logs are an observable part of system behavior. By distinguishing between diagnostic logging for developers and support logging for business needs, developers can ensure effective logging strategies. The use of structured logging and well-defined logging interfaces simplifies testing and improves maintainability.