SnapshotAssertions.TUnit
0.7.1
Prefix Reserved
dotnet add package SnapshotAssertions.TUnit --version 0.7.1
NuGet\Install-Package SnapshotAssertions.TUnit -Version 0.7.1
<PackageReference Include="SnapshotAssertions.TUnit" Version="0.7.1" />
<PackageVersion Include="SnapshotAssertions.TUnit" Version="0.7.1" />
<PackageReference Include="SnapshotAssertions.TUnit" />
paket add SnapshotAssertions.TUnit --version 0.7.1
#r "nuget: SnapshotAssertions.TUnit, 0.7.1"
#:package SnapshotAssertions.TUnit@0.7.1
#addin nuget:?package=SnapshotAssertions.TUnit&version=0.7.1
#tool nuget:?package=SnapshotAssertions.TUnit&version=0.7.1
SnapshotAssertions.TUnit
Scope: Test projects only. Not intended for production code.
TUnit-native text-snapshot assertions on top of TUnit's [AssertionExtension] source generator. AOT-compatible, trimmable, no reflection. Coexists with Verify; does not replace it for object-graph cases.
Full documentation, full options reference, design notes, and roadmap: github.com/JohnVerheij/SnapshotAssertions.TUnit
Install
dotnet add package SnapshotAssertions.TUnit
SnapshotAssertions (the framework-agnostic core) comes transitively. Requirements: TUnit 1.54.0 or later, .NET 10.
Quick start
using SnapshotAssertions;
using PublicApiGenerator;
[Test]
public async Task Public_api_surface_matches_baseline()
{
var assembly = typeof(MyLib.Foo).Assembly;
var actual = ApiGenerator.GeneratePublicApi(assembly);
await Assert.That(actual).MatchesSnapshot();
}
The default file resolver writes Snapshots/{TestClassName}.{TestMethodName}.expected.txt. On mismatch, *.actual.txt is written next to the expected file and the assertion failure includes both paths plus a line-based diff.
Accept-changes workflow
Three modes, in order of preference:
- IDE diff-and-merge. Most IDEs (Rider, VS Code) detect side-by-side
.expected.txtand.actual.txtfiles and offer a diff-and-merge view. - Manual
cp.cp Snapshots/MyTest.actual.txt Snapshots/MyTest.expected.txt. - Bulk accept.
SNAPSHOT_ACCEPT=1 dotnet test. Refuses to run ifCI=true(so a slipped pipeline env never accepts silently).
As of v0.6.0, bulk accept writes the new baseline straight into the source Snapshots/ folder (resolved via SnapshotFileResolver.TryResolveSourceSnapshotsDirectory), so the committed baseline updates in place even under dotnet test --no-build. It falls back to the test binary's directory only when the source path cannot be resolved.
CI never sets SNAPSHOT_ACCEPT. Mismatches always fail the build in pipelines.
Scrubbers
For snapshots that contain volatile values (GUIDs, ISO 8601 timestamps, Unix-epoch-millis numbers, request IDs, etc.), chain .WithScrubber(...) calls to replace them with stable indexed tokens before comparison. Recurring values share an index; different kinds maintain independent counters.
using SnapshotAssertions;
// Curated default: Guid + Iso8601Timestamp + UnixEpochMillis
await Assert.That(jsonResponse)
.MatchesSnapshot()
.WithScrubber(Scrubbers.Default);
// Extended curated chain (v0.4.0+): adds GuidN + ElapsedMs to the Default set
await Assert.That(diagnostic)
.MatchesSnapshot()
.WithScrubber(Scrubbers.Common);
// Custom regex: replace request-id headers with a literal token
await Assert.That(httpLog)
.MatchesSnapshot()
.WithScrubber(Scrubbers.Pattern(@"\brequest-id=[a-f0-9-]+", "request-id=<scrubbed>"));
// Custom regex with correlation kept (v0.5.0+): recurring values share <kind:N>
await Assert.That(text)
.MatchesSnapshot()
.WithScrubber(Scrubbers.IndexedPattern(@"\bticket-\d+\b", "ticket"));
// Assemble a reusable bundle once; pass as a single scrubber (v0.3.0+)
private static readonly SnapshotScrubber FixturesScrubber = Scrubbers.Combine(
Scrubbers.Common,
Scrubbers.Pattern(@"\brequest-id=[a-f0-9-]+", "request-id=<scrubbed>"));
The built-in indexed scrubbers emit <kind:N> tokens where N is assigned by first-occurrence order per kind. The same value at every site keeps the same N. Scrubbers.Pattern(...) overloads emit a literal token (no indexing); Scrubbers.IndexedPattern(...) (v0.5.0+) is the indexed counterpart for custom regex, so recurring matched values keep a shared <kind:N> index. Scrubbers.Combine(...) (v0.3.0+) wraps an array of scrubbers into a single composite so a reused bundle does not have to be re-chained on every assertion. Scrubbers.Common (v0.4.0+) is the extended curated chain: Guid + GuidN + Iso8601Timestamp + UnixEpochMillis + ElapsedMs. Reach for Common first; fall back to Default for the v0.3.0-and-earlier three-pattern chain only when an existing baseline depends on it.
Full Scrubbers reference, custom-scrubber recipe, and design notes on GitHub.
Smart-diff suggestions in failure messages
On a snapshot mismatch, the failure message now scans the rendered diff for known volatile patterns and recommends applicable built-in scrubbers automatically. No configuration is required. Wider diffs that match many patterns get a top-3 list plus a ... and N more rollup, so the failure message stays scannable.
Smart-diff suggestions reference on GitHub.
Renderer pattern for typed values
For values that are not already strings, project them via a renderer:
// Inline delegate projection.
await Assert.That(myProto)
.MatchesSnapshot(p => Formatter.Format(p))
.WithScrubber(Scrubbers.Common);
// Reusable subclass for project-wide canonical renderers.
internal sealed class MyProtoRenderer : SnapshotRenderer<MyProto>
{
public override string Render(MyProto value) => Formatter.Format(value);
}
// ...
await Assert.That(myProto).MatchesSnapshot(new MyProtoRenderer());
The two overloads enable sibling family packages (LogAssertions.TUnit, MathAssertions.TUnit, etc.) to publish renderers for their own types as static helper methods without taking a reference on SnapshotAssertions. Consumers compose at the test call site via the delegate overload.
Renderer pattern reference and sibling-family composition recipe on GitHub.
Parameterized tests ([Arguments])
For parameterized tests, the default file resolver hashes the row's argument values and appends an 8-hex-character suffix to the snapshot file name, so each row gets a distinct baseline:
[Test]
[Arguments("alpha", 200)]
[Arguments("beta", 404)]
public async Task Response_per_route_matches(string route, int statusCode)
{
await Assert.That(RenderResponse(route, statusCode)).MatchesSnapshot();
}
Baselines land at Snapshots/{TestClassName}.{TestMethodName}.{ArgsHash8}.expected.txt. The hash is InvariantCulture-stable so the same arguments produce the same file across developer machines and CI. Collection arguments are expanded element-by-element (since v0.7.0), so rows that differ only inside an array get distinct files. Full details on GitHub.
Why not Verify
Verify is excellent for object-graph diffing, scrubbers, IDE-integrated diff display. It remains the right choice when those features matter. SnapshotAssertions covers the text-snapshot 80% case without:
- Verify's
<Deterministic>false</Deterministic>requirement (which on Linux runners breaksMicrosoft.CodeCoverage's instrumentation pipeline; documented at TUnit#4149) - The 30-50 lines of per-project file-compare scaffolding consumers otherwise reproduce in every repo
The two libraries can coexist in the same test project; this package does not depend on Verify.
Family
Part of an assertion family for TUnit:
- LogAssertions.TUnit
- TimeAssertions.TUnit
- MathAssertions.TUnit
- JsonAssertions.TUnit
- SseAssertions.TUnit
- GrpcAssertions.TUnit
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
- SnapshotAssertions (>= 0.7.1)
- TUnit.Assertions (>= 1.54.0)
- TUnit.Core (>= 1.54.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
View the rendered release notes: https://github.com/JohnVerheij/SnapshotAssertions.TUnit/releases/tag/v0.7.1
Patch release. Restores `build/SnapshotAssertions.TUnit.targets` to the package. 0.7.0 shipped without it, so a consumer's committed `Snapshots/*.expected.txt` baselines stopped copying to the test output directory and every snapshot assertion failed with "baseline does not exist". No public API change.
### Fixed
- **The package again ships `build/SnapshotAssertions.TUnit.targets`.** DotNetProjectFile.Analyzers 1.15.0 added `**/*.targets` to a SonarQube content glob marked `Pack="false"`, which silently overrode this package's explicit `Pack="true"` for its own build targets and dropped the file from the 0.7.0 `.nupkg`. That targets file auto-includes a consumer's `Snapshots/**/*.expected.txt` with `CopyToOutputDirectory="PreserveNewest"`, so without it the committed baselines never reach the test binary's output directory and the resolver reports them missing. Setting `SonarQubeIntegration=false` (the integration is unused in this package) restores the packed asset, and a CI check now asserts the produced `.nupkg` contains the build targets so it cannot silently drop again.
### Upgrade note
Consumers who applied the 0.7.0 workaround (a manual `<None Update="Snapshots/**/*.expected.txt" CopyToOutputDirectory="PreserveNewest" />` together with `<SnapshotAssertionsAutoIncludeSnapshots>false</SnapshotAssertionsAutoIncludeSnapshots>`) can remove both after upgrading; the package's own targets resume handling the copy. Leaving the workaround in place stays harmless.