SnapshotAssertions.TUnit
0.4.0
Prefix Reserved
dotnet add package SnapshotAssertions.TUnit --version 0.4.0
NuGet\Install-Package SnapshotAssertions.TUnit -Version 0.4.0
<PackageReference Include="SnapshotAssertions.TUnit" Version="0.4.0" />
<PackageVersion Include="SnapshotAssertions.TUnit" Version="0.4.0" />
<PackageReference Include="SnapshotAssertions.TUnit" />
paket add SnapshotAssertions.TUnit --version 0.4.0
#r "nuget: SnapshotAssertions.TUnit, 0.4.0"
#:package SnapshotAssertions.TUnit@0.4.0
#addin nuget:?package=SnapshotAssertions.TUnit&version=0.4.0
#tool nuget:?package=SnapshotAssertions.TUnit&version=0.4.0
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.44.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).
CI never sets SNAPSHOT_ACCEPT. Mismatches always fail the build in pipelines.
Scrubbers (v0.2.0+)
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>"));
// 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.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 (v0.4.0+)
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 (v0.4.0+)
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. 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:
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.4.0)
- TUnit.Assertions (>= 1.44.39)
- TUnit.Core (>= 1.44.39)
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.4.0
Additive release. No breaking changes; baselines that opted into `Scrubbers.Default` produce byte-identical output.
### Added
- `Scrubbers.GuidN`: scrubs 32-character GUID-N format strings (the `Guid.ToString("N")` shape) into `<guid:N>` tokens. Shares the `"guid"` kind name with `Scrubbers.Guid`, so the index counter is unified across both formats; the Nth GUID occurrence in a snapshot gets the same N regardless of which format produced it.
- `Scrubbers.ElapsedMs`: scrubs elapsed-millisecond values (`42ms`, `42.5ms`, `1234.567 ms`) into `<elapsed-ms:N>` tokens. Case-sensitive on the `ms` suffix.
- `Scrubbers.Common`: curated chain of `Guid` + `GuidN` + `Iso8601Timestamp` + `UnixEpochMillis` + `ElapsedMs`. Superset of `Scrubbers.Default`; opt-in for the extended pattern set. Ordering follows the most-specific-first rule to avoid double-scrubbing.
- `DiffSuggestionAnalyzer.Analyze(string diff)`: scans a snapshot-mismatch diff for known volatile patterns and returns `DiffSuggestion` entries recommending applicable built-in scrubbers, ordered by hit count descending with stable secondary ordering by declaration order. Counts matches only on lines that begin with `+` or `-`; context lines are skipped so patterns unchanged on both sides do not surface as suggestions.
- `DiffSuggestion(string PatternName, int Count, string Recommendation)` record: one scrubber recommendation surfaced by `DiffSuggestionAnalyzer`.
- `SnapshotResult.Describe()` / `WriteDescription` smart-diff suggestion section: on `Mismatched` outcomes with detected patterns, the failure message now includes a "Suggestion(s)" section between the diff and the accept-flow guidance. The list is capped at the top 3 patterns by hit count; surplus patterns roll up into an "... and N more" line pointing consumers at `Scrubbers.Common`. Failure messages for `Mismatched` outcomes with no detected patterns, and for all other outcomes (`Matched`, `NoBaseline`, `Accepted`), are byte-identical to v0.3.0.
- `SnapshotAssertions.Render.SnapshotRenderer<T>` abstract base class: consumers subclass to plug domain types (Google.Protobuf messages, `XDocument`, `Activity`, project-specific value objects) into the snapshot pipeline.
- `SnapshotAssertions.Render.Renderer.For<T>(Func<T, string>)` static factory: builds a renderer from a lambda when a full subclass is unnecessary.
- `RenderedSnapshotAssertion<T>`: renderer-projected assertion type returned by the two new `MatchesSnapshot<T>` overloads. Carries the same chain methods as `SnapshotAssertion` (`WithName`, `AtPath`, `WithOptions`, `WithScrubber`).
- `MatchesSnapshot<T>(this IAssertionSource<T>, SnapshotRenderer<T>)` extension: renderer-projected entry point taking a subclass renderer. Use when the renderer is reusable and owns configuration or state.
- `MatchesSnapshot<T>(this IAssertionSource<T>, Func<T, string>)` extension: delegate-shaped entry point. Use for inline projections and for composing with a sibling family package's static renderer method without taking a reference on its assembly.
### Changed
- `TUnit` package reference bumped `1.44.0` → `1.44.39` (and the external-consumer smoke-test pin). 1.44.39 carries the `[GenerateAssertion]` source-generator fix for value-type optional parameters; no behavioural change for this package, taken for family lockstep.
- `Microsoft.SourceLink.GitHub` bumped `10.0.203` → `10.0.300`. The embedded source-link metadata in shipped `.pdb` files now points at the updated SourceLink schema; debugging-into-the-package from consumers' IDEs is unaffected in behaviour but uses the newer SourceLink format.
- README cookbook documents `Scrubbers.Common` ordering rationale, smart-diff suggestion output shape and top-3 cap, the renderer-pattern API with four worked subclass examples (`OtelTraceIdScrubber`, `EphemeralPathScrubber`, `PortScrubber`, `NumericTokenScrubber` as a parameterised variant), and sibling-family composition without cross-package dependency.
- Packaged READMEs (`src/SnapshotAssertions.TUnit/README.md`, `src/SnapshotAssertions/README.md`) mention `Scrubbers.Common`, smart-diff suggestions, and the renderer-pattern API with deep-links to the root README.