Text · HTML · JSON · YAML · Images · Browser · HTTP

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.

● ● ●dotnet test
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

Seven formats

Text stored as UTF-8 with LF endings, images as given, each compared its own way.

FormatAssertionsCompared
TextMatchSnapshotline by line, shown as unified hunks
HTMLMatchHtmlSnapshot, BeHtml, ContainHtmlas a DOM with AngleSharp.Diffing; select, remove and transform first
JSONMatchJsonSnapshot, BeJsonas a tree: keys in any order, numbers by value, arrays aligned item by item
YAMLMatchYamlSnapshot, BeYamlas a tree with the YAML 1.2 core schema, stored as written, comments and all
ImagesMatchImageSnapshotby pixels, perceptually, with anti-aliasing left out and a diff image on failure
Browser journeysMatchJourneySnapshot, MatchAriaSnapshotagent-browser commands run in Chrome; each read compared as a tree
HTTPMatchHttpSnapshot, 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.

● ● ●journeys/shop.journey
# 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
● ● ●ShopJourneys.cs
[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 →

● ● ●.snapshots/journeys/shop.verified.journey
## 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.

● ● ●http/orders.http
### 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
● ● ●ApiTests.cs
[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 →

● ● ●.snapshots/http/orders.verified.responses
## 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.

● ● ●BasketTests.cs
[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 →

● ● ●dotnet test
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
$ 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.

● ● ●install
dotnet tool install --global Nullean.Snapshots.Cli
# or, without installing:
dnx Nullean.Snapshots.Cli status
● ● ●snap
[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