Skip to main content

Logging

TUnit.Mocks.Logging provides MockLogger — a simple ILogger implementation that captures log entries for inspection and verification.

dotnet add package TUnit.Mocks.Logging --prerelease
No Source Generation Needed

Unlike TUnit.Mocks, the logging helpers are plain classes — no source generation required. Create loggers with Mock.Logger() and pass them to your code.

Getting Started

using TUnit.Mocks;
using Microsoft.Extensions.Logging;

[Test]
public async Task Service_Logs_On_Startup()
{
// Arrange
var logger = Mock.Logger();
var service = new MyService(logger);

// Act
service.Start();

// Assert
logger.VerifyLog(LogLevel.Information, "started", Times.Once);
}

Creating a Logger

// Untyped logger
var logger = Mock.Logger();
ILogger iLogger = logger;

// With category name
var logger = Mock.Logger("MyApp.Services");

// Generic typed logger (implements ILogger<T>)
var logger = Mock.Logger<MyService>();
ILogger<MyService> iLogger = logger;

Inspecting Entries

logger.LogInformation("User {UserId} logged in", 42);
logger.LogWarning("Disk space low");

// All entries
await Assert.That(logger.Entries).HasCount().EqualTo(2);
await Assert.That(logger.Entries[0].LogLevel).IsEqualTo(LogLevel.Information);
await Assert.That(logger.Entries[0].Message).Contains("42");

// Most recent entry
var latest = logger.LatestEntry;
await Assert.That(latest!.Message).IsEqualTo("Disk space low");

Each LogEntry provides:

PropertyDescription
LogLevelThe log level (Trace, Debug, Information, etc.)
EventIdThe event ID
MessageThe formatted log message
ExceptionThe associated exception (or null)
TimestampWhen the entry was recorded
CategoryNameThe logger's category name

Verification

Fluent API

Build verification queries with filters:

// By level
logger.VerifyLog().AtLevel(LogLevel.Error).WasCalled(Times.Once);

// By message content (contains)
logger.VerifyLog().ContainingMessage("failed").WasCalled(Times.Once);

// By exact message
logger.VerifyLog().WithMessage("Operation completed").WasCalled(Times.Once);

// By exception type
logger.VerifyLog().WithException<InvalidOperationException>().WasCalled(Times.Once);

// Combined filters
logger.VerifyLog()
.AtLevel(LogLevel.Error)
.WithException<InvalidOperationException>()
.ContainingMessage("database")
.WasCalled(Times.Once);

Shorthand Methods

// Verify message at level (at least once)
logger.VerifyLog(LogLevel.Error, "connection failed");

// Verify message at level with count
logger.VerifyLog(LogLevel.Warning, "retry", Times.Exactly(3));

// Verify nothing logged at a level
logger.VerifyNoLog(LogLevel.Error);

// Verify nothing logged at all
logger.VerifyNoLogs();

Never Called

logger.VerifyLog().AtLevel(LogLevel.Error).WasNeverCalled();

Filtering Entries

Retrieve entries matching specific criteria:

// By level
var errors = logger.GetLogs(LogLevel.Error);

// By message content
var retryLogs = logger.GetLogs("retry");

// Using the fluent API
var matching = logger.VerifyLog()
.AtLevel(LogLevel.Warning)
.ContainingMessage("timeout")
.GetMatchingEntries();

Reset

logger.Clear(); // removes all captured entries

Dependency Injection

Pass Mock.Logger<T>() anywhere ILogger<T> is expected:

[Test]
public async Task OrderService_Logs_Errors()
{
var logger = Mock.Logger<OrderService>();
var service = new OrderService(logger);

await service.ProcessOrder(invalidOrder);

logger.VerifyLog()
.AtLevel(LogLevel.Error)
.ContainingMessage("validation failed")
.WasCalled(Times.Once);
}