LogAssertions.TUnit
0.4.0
Prefix Reserved
dotnet add package LogAssertions.TUnit --version 0.4.0
NuGet\Install-Package LogAssertions.TUnit -Version 0.4.0
<PackageReference Include="LogAssertions.TUnit" Version="0.4.0" />
<PackageVersion Include="LogAssertions.TUnit" Version="0.4.0" />
<PackageReference Include="LogAssertions.TUnit" />
paket add LogAssertions.TUnit --version 0.4.0
#r "nuget: LogAssertions.TUnit, 0.4.0"
#:package LogAssertions.TUnit@0.4.0
#addin nuget:?package=LogAssertions.TUnit&version=0.4.0
#tool nuget:?package=LogAssertions.TUnit&version=0.4.0
LogAssertions.TUnit
Scope: Test projects only. Not intended for production code.
TUnit-native fluent log-assertion DSL on top of Microsoft.Extensions.Logging.Testing.FakeLogCollector. AOT-compatible, trimmable, no reflection.
Full documentation, full filter reference, design notes, and roadmap: github.com/JohnVerheij/LogAssertions.TUnit
Install
dotnet add package LogAssertions.TUnit
LogAssertions (the framework-agnostic core) comes transitively. Requirements: TUnit 1.43.11+, .NET 10.
Quick start
using LogAssertions;
using Microsoft.Extensions.Logging;
[Test]
public async Task Validation_failure_is_logged()
{
var (factory, collector) = LogCollectorBuilder.Create();
using (factory)
{
var logger = factory.CreateLogger<MyValidator>();
new MyValidator(logger).Validate(invalidInput);
await Assert.That(collector)
.HasLogged()
.AtLevel(LogLevel.Warning)
.Containing("validation failed", StringComparison.Ordinal)
.Once();
await Assert.That(collector).HasNotLogged().AtLevelOrAbove(LogLevel.Error);
}
}
Entry points
| Method | Default expectation |
|---|---|
HasLogged() |
At least 1 matching record |
HasNotLogged() |
Zero matching records |
HasLoggedSequence() |
Records appear in order; Then() separates steps |
Plus shorthands: HasLoggedOnce(), HasLoggedExactly(int), HasLoggedAtLeast(int), HasLoggedBetween(int, int), HasLoggedNothing(), HasLoggedWarningOrAbove(), HasLoggedErrorOrAbove().
Filters chain with AND semantics: AtLevel, AtLevelOrAbove, Containing, WithException<T>, WithInnerException<T> (v0.4.0+), WithInnerExceptionMessage (v0.4.0+), WithProperty, WithCategory, WithEventId, WithScope<T>, WithScopeProperty, WithScopeProperties (v0.4.0+), plus combinators MatchingAny/MatchingAll/Not/WithFilter for composable filter objects. Sequence assertions chain via Then() (strict order) or ThenAnyOrder(...) (v0.4.0+) (concurrent group; sub-steps may match in any order). Full filter reference on GitHub.
Cookbook
Assert no errors were logged:
await Assert.That(collector).HasNotLogged().AtLevelOrAbove(LogLevel.Error);
Assert a specific call site was hit (anchored on the message template, not the substituted value):
await Assert.That(collector).HasLogged()
.WithMessageTemplate("Order {OrderId} processed").AtLeast(1);
Assert a specific exception flowed through a logger:
await Assert.That(collector).HasLogged()
.AtLevel(LogLevel.Error)
.WithException<DbUpdateConcurrencyException>()
.Once();
Assert a wrapped exception (gRPC / RPC pattern, v0.4.0+):
await Assert.That(collector).HasLogged()
.WithException<RpcException>()
.WithInnerException<TimeoutException>()
.Once();
Assert a startup → work → shutdown sequence:
await Assert.That(collector).HasLoggedSequence()
.WithEventName("Startup")
.Then().AtLevel(LogLevel.Information).Containing("processed", StringComparison.Ordinal)
.Then().WithEventName("Shutdown");
Assert a fan-out completion in any order (v0.4.0+):
await Assert.That(collector).HasLoggedSequence()
.Containing("Request received", StringComparison.Ordinal)
.ThenAnyOrder(
s => s.Containing("Auth check passed", StringComparison.Ordinal),
s => s.Containing("Quota check passed", StringComparison.Ordinal))
.Then().Containing("Response sent", StringComparison.Ordinal);
Assert several invariants and report all failures together:
await Assert.That(collector).AssertAllAsync(
c => c.HasLogged().AtLevel(LogLevel.Information).AtLeast(1),
c => c.HasNotLogged().AtLevelOrAbove(LogLevel.Error),
c => c.HasLoggedSequence().WithEventName("Startup").Then().WithEventName("Shutdown"));
Failure diagnostics
On a failed assertion, the exception message includes the expected match count, the actual count, and a snapshot of every captured record (level abbreviation, category, message, structured properties, scopes, exception). No need for Console.WriteLine debugging — every dimension you can filter on is also rendered in the failure message.
Full failure-diagnostics example, design notes, stability intent, and roadmap on GitHub.
License
MIT — Copyright (c) 2026 John Verheij
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- LogAssertions (>= 0.4.0)
- Microsoft.Extensions.Diagnostics.Testing (>= 10.5.0)
- TUnit.Assertions (>= 1.43.11)
- TUnit.Core (>= 1.43.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
See CHANGELOG.md