Structural Snapshot tests for .NET
Snapshots on AwesomeAssertions, compared by structure and reported by path, in plain messages that read the same in a terminal, a CI log and an agent's context.
JSON did not match snapshot: tests/Shop.Tests/.snapshots/CatalogTests/Serializes.verified.json 3 difference(s): $.items[0].name expected "kibana" actual "Kibana" (verified.json:5:15) $.items[1] missing in actual (verified.json:7:5) $.extra unexpected in actual Received: .artifacts/.snapshots/received/Shop.Tests/CatalogTests/Serializes.received.json Accept: snap accept Shop.Tests/CatalogTests/Serializes
Small, strict, readable
A snapshot test is only as good as the moment you look at its failure. Everything here is built for that moment.
No colour codes, repository-relative paths, at most 20 differences and 80 lines, and the exact command to accept.
Attribute order, whitespace, key order and 1 versus 1.0 do not fail a test. A changed field is reported by its path.
No auto-accept, ever. SNAPSHOT_ACCEPT=1 works on your machine; in CI it is ignored and the failure says so.
Verified files under .snapshots/, mirroring your tests. Received files in obj/ or your artifacts folder, never in source.
snap reviews, accepts, rejects and finds orphaned snapshots, native on Linux, macOS and Windows.
No heavy core, and every write is scoped to the snapshot folders, so even a bug cannot touch anything else.
Seven formats
Text stored as UTF-8 with LF endings, images as given, each compared its own way.
| Format | Assertions | Compared |
|---|---|---|
| Text | MatchSnapshot | line by line, shown as unified hunks |
| HTML | MatchHtmlSnapshot, BeHtml, ContainHtml | as a DOM with AngleSharp.Diffing; select, remove and transform first |
| JSON | MatchJsonSnapshot, BeJson | as a tree: keys in any order, numbers by value, arrays aligned item by item |
| YAML | MatchYamlSnapshot, BeYaml | as a tree with the YAML 1.2 core schema, stored as written, comments and all |
| Images | MatchImageSnapshot | by pixels, perceptually, with anti-aliasing left out and a diff image on failure |
| Browser journeys | MatchJourneySnapshot, MatchAriaSnapshot | agent-browser commands run in Chrome; each read compared as a tree |
| HTTP | MatchHttpSnapshot, MatchHttpFileSnapshot | .http files run in order; status, chosen headers and JSON bodies compared as trees, secrets redacted |
Browser journeys
Click through a running site with Nullean.Cdp: a text file of agent-browser commands, run in Chrome with no Node and no daemon. Every read lands in one transcript per journey, compared as a tree, so a failure names the node that moved.
# Search, add a lamp, look at the basket. open index.html find label Search fill lamp find role button click --name Search --exact snapshot -c -s "#results" find role button click --name "Add red lamp" get text #count click "a[href='basket.html']" get title snapshot -c -s main
[Test] [JourneyFiles("journeys")] // a test case per file public Task Run(SnapshotFile journey) => JourneyTest.RunAsync(journey, new() { BaseUrl = server.Url });
Add a11y, links, network requests --failed or Web Vitals budgets and
they snapshot too; screenshots and HAR
files land next to the received transcript. Browser journeys →
## snapshot -c -s #results url: {origin}/index.html - list - listitem [level=1] - StaticText "Red lamp " - button "Add red lamp" - listitem [level=1] - StaticText "Blue lamp " - button "Add blue lamp" ## get text #count url: {origin}/index.html > 1 ## get title url: {origin}/basket.html > Basket ## snapshot -c -s main url: {origin}/basket.html - main - heading "Basket" [level=1] - paragraph - StaticText "Your basket is empty." - link "Back to the shop"
HTTP APIs
Run the .http files you already keep for Visual Studio, Rider or the REST Client against any
HttpClient, WebApplicationFactory's included. Each response is snapshotted:
status, the headers you choose, and JSON bodies compared as trees.
### login POST {{host}}/api/login Content-Type: application/json {"user": "ada"} ### GET {{host}}/api/orders Authorization: Bearer {{login.response.body.$.token}} ### GET {{host}}/api/missing
[Test] [HttpFiles("http")] public Task Run(SnapshotFile file) => HttpFileTest.RunAsync(file, new() { Client = factory.CreateClient() }); [Test] public async Task Orders() => await (await client.GetAsync("/api/orders")) .Should().MatchHttpSnapshot();
Secrets from user secrets, configuration or environment variables are redacted from everything written,
even when a server echoes them back. snap http run runs the same files against a live host.
HTTP →
## login: POST {{host}}/api/login status: 200 OK content-type: application/json; charset=utf-8 { "token": "tok-0123456789", "user": { "name": "ada", "roles": [ "admin", "dev" ] } } ## GET {{host}}/api/orders status: 200 OK content-type: application/json; charset=utf-8 { "orders": [ { "id": 1, "total": 12.5 }, { "id": 2, "total": 3 } ] } ## GET {{host}}/api/missing status: 404 Not Found content-type: text/plain; charset=utf-8 > not found
Inline snapshots, accepted by snap
Keep a short expected value in the test, as a string literal, for text, HTML, JSON and YAML. When the output
changes, snap accept rewrites that literal in your source itself: a plain string for one line, a raw
""" literal for several.
No agent edits, no tokens spent. It is a deterministic edit by the CLI, and there are no expected strings to retype by hand.
[Test] public void RendersTheBasket() => basket.Render().Should().MatchInlineSnapshot(""" 2 × beats 1 × lamp """); [Test] public void Serializes() => // no value yet: snap accept writes it json.Should().MatchInlineJsonSnapshot();
snap review shows each change as a diff and snap accept --all takes the lot, or
SNAPSHOT_ACCEPT=1 dotnet test does it without the CLI. Nothing is edited until you accept, and never
in CI. If the test file changed since the run, or the expected value is not a plain literal, accepting says why
and edits nothing. Inline snapshots →
Text did not match inline snapshot: tests/Shop.Tests/BasketTests.cs:14 1 difference(s): @@ line 1 @@ 2 × beats -1 × lamp +1 × kibana Received: .artifacts/.snapshots/received/Shop.Tests/BasketTests/RendersTheBasket.L14.received.inline Accept: snap accept Shop.Tests/BasketTests/RendersTheBasket.L14
$ snap accept Shop.Tests/BasketTests/RendersTheBasket.L14 accepted Shop.Tests/BasketTests/RendersTheBasket.L14 -> tests/Shop.Tests/BasketTests.cs:14 $ git diff tests/Shop.Tests/BasketTests.cs 2 × beats - 1 × lamp + 1 × kibana """);
Review, then accept
snap walks through what changed, one snapshot at a time. Accept it, reject it, or skip it for
later. Keys can be given in full, as a unique suffix, or as a glob.
dotnet tool install --global Nullean.Snapshots.Cli # or, without installing: dnx Nullean.Snapshots.Cli status
[1/2] changed Shop.Tests/Rendering/CatalogTests/Renders .html --- tests/Shop.Tests/.snapshots/Rendering/CatalogTests/Renders.verified.html +++ .artifacts/.snapshots/received/Shop.Tests/Rendering/CatalogTests/Renders.received.html @@ line 4 @@ <tr> - <td>kibana</td> + <td>Kibana</td> </tr> [a]ccept, [r]eject, [s]kip, [q]uit? a accepted Shop.Tests/Rendering/CatalogTests/Renders