Snapshot Testing skill for Codex

Snapshots small enough to actually review. Codex snapshots only outputs that are hard to assert field by field, keeps them small and deterministic by redacting ids, freezing time and sorting collections, and prefers inline or file snapshots that reviewers can read. It configures CI to fail on missing snapshots, updates only the tests it changed, and cleans up noisy suites by shrinking, stabilising or converting snapshots to explicit assertions.

  • Testing & QA
  • $snapshot-testing
  • 188 lines
  • 2 files
  • ~1,700 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- snapshot-testing

Installs to ~/.agents/skills/snapshot-testing/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $snapshot-testing, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Keeps snapshot and golden-file tests useful - Jest and Vitest toMatchSnapshot, inline and file snapshots, Python syrupy and inline-snapshot, Rust insta, Go golden files with an -update flag - by snapshotting small deterministic outputs, redacting ids and timestamps with property matchers or serializers, failing on missing snapshots in CI, and reviewing every update as a code change. Use when adding snapshot tests, when snapshots churn on every change, when reviewers rubber-stamp huge .snap diffs, or when deciding between a snapshot and an explicit assertion.

Use it when

  • Adding tests for serialised output, generated code or CLI text
  • Snapshot files churn on unrelated changes
  • Reviewers approve large snapshot diffs without reading them
  • Deciding between a snapshot and explicit assertions

Use something else when

How to check the result

A small behaviour change fails the snapshot test with a readable diff, snapshots stay identical across time zones and CI, and CI fails rather than writing a missing snapshot.

The mistake it prevents

Running the update flag across the whole suite to get green, which silently accepts every regression that was failing at the time.

~/.agents/skills/snapshot-testing/SKILL.md

--- name: snapshot-testing description: Keeps snapshot and golden-file tests useful - Jest and Vitest toMatchSnapshot, inline and file snapshots, Python syrupy and inline-snapshot, Rust… license: MIT ---

A snapshot test is useful when the snapshot is small enough to read in review, contains only deterministic output that matters, and changes only when behaviour changes. If the team runs -u without reading the diff, the tests assert nothing.

Inspect first

  1. Read AGENTS.md; find existing snapshots: rg --files -g '*.snap' -g '__snapshots__/**' -g '*.snap.new' -g 'testdata/*.golden' and count their size (wc -l) to find the giant ones.
  2. Check churn: git log --since=3.months --format= --name-only -- '*.snap' | sort | uniq -c | sort -rn | head. Files that change in most commits are noise generators.
  3. Check CI flags: Jest --ci, Vitest in CI mode, INSTA_UPDATE, syrupy options. CI must fail on a missing snapshot, never write one.

When to snapshot

OutputSnapshot?
Serialised structures with many fields: API responses, generated config, AST, SQL from a query builder, CLI output, error messagesYes, ideal
Code generators, formatters, compilers, migrations generated from modelsYes, golden files
One or two key values (total, status, a flag)No, assert explicitly
Whole rendered component trees with styling classesNo; test behaviour with Testing Library, appearance with visual tests
Output containing time, random ids, ordering of unordered collectionsOnly after redaction and sorting
PixelsNo; that is visual-regression-testing

Rule of thumb: if a reviewer cannot tell from the diff whether the change is correct within a minute, the snapshot is too big or too noisy.

JavaScript: Jest and Vitest

it("serialises an invoice for the API", () => {
  const json = toApiInvoice(invoiceFixture());
  expect(json).toMatchSnapshot({
    id: expect.any(String),
    issuedAt: expect.any(String),
  });
});

it("formats the CLI summary", () => {
  expect(formatSummary(results)).toMatchInlineSnapshot(`
    "3 passed, 1 failed (1.2s)
    FAIL src/cart.test.ts > applies promo"
  `);
});
  • Prefer toMatchInlineSnapshot for small outputs: the expected value sits in the test and reviewers see it in context.
  • Vitest await expect(html).toMatchFileSnapshot("./__snapshots__/email.html") keeps large outputs in a real file with syntax highlighting.
  • Property matchers (expect.any(String)) replace unstable fields; for repeated noise across many tests, write a serializer:
// vitest.setup.ts or jest setup
expect.addSnapshotSerializer({
  test: (v) => typeof v === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(v),
  serialize: () => '"<uuid>"',
});
  • Update deliberately: vitest -u -t "invoice" or jest -u -t "invoice" for the tests you changed, not the whole suite.
  • CI: jest --ci fails when a snapshot is missing instead of writing it; Vitest does not write new snapshots when CI is set. Remove obsolete snapshots with -u locally after deleting tests.

Python: syrupy and inline-snapshot

from syrupy.filters import props

def test_invoice_payload(snapshot):
    payload = to_api_invoice(make_invoice())
    assert payload == snapshot(exclude=props("id", "issued_at"))
pytest --snapshot-update tests/test_invoice.py    # writes __snapshots__/*.ambr; review the diff

For small values, inline-snapshot writes the expected value into the source:

from inline_snapshot import snapshot

def test_slug():
    assert slugify("Hello, World!") == snapshot("hello-world")

pytest --inline-snapshot=create fills empty snapshot() calls, --inline-snapshot=fix updates failing ones, --inline-snapshot=review asks per change.

Rust: insta

#[test]
fn renders_migration_sql() {
    let sql = plan_migration(&old_schema(), &new_schema());
    insta::assert_snapshot!(sql);
}

#[test]
fn api_response_shape() {
    insta::assert_json_snapshot!(response(), { ".id" => "[id]", ".created_at" => "[ts]" });
}

cargo insta test then cargo insta review to accept or reject each .snap.new (cargo install cargo-insta, needs network). JSON snapshots and redactions need the crate’s json and redactions features in Cargo.toml. In CI set INSTA_UPDATE=no so nothing is written.

Go: golden files

var update = flag.Bool("update", false, "rewrite golden files")

func TestRenderReport(t *testing.T) {
	got := RenderReport(sampleData())
	golden := filepath.Join("testdata", t.Name()+".golden")
	if *update {
		if err := os.WriteFile(golden, got, 0o644); err != nil {
			t.Fatal(err)
		}
	}
	want, err := os.ReadFile(golden)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(got, want) {
		t.Errorf("output differs from %s; run go test -run %s -update and review the diff", golden, t.Name())
	}
}

Run go test ./internal/report -run TestRenderReport -update. Use cmp.Diff from go-cmp for readable structured diffs.

Making output deterministic

  • Freeze the clock and seed randomness in the test.
  • Sort unordered collections (maps, sets, rows without ORDER BY) before snapshotting.
  • Normalise environment-specific values: absolute paths, line endings, OS-specific separators, locale formatting, versions.
  • Redact generated ids and tokens with matchers, filters or serializers, never by hand-editing the snapshot file.

Review rules

  • Treat .snap, .ambr and .golden diffs as code: read every changed line.
  • A PR that updates snapshots says why in the description (“added currency field to the invoice payload”).
  • Snapshot updates mixed with unrelated changes get split or questioned.
  • If a snapshot file exceeds roughly 100 lines per test, shrink the subject or switch to explicit assertions.

Fixing a noisy suite

  1. Rank snapshot files by churn and size.
  2. For each top offender, decide: shrink (snapshot a sub-object), stabilise (redact and sort), convert (explicit assertions for the 2-3 facts that matter), or delete (covered elsewhere).
  3. Remove obsolete snapshots; most runners can report them.
  4. Add the CI flag that fails on missing snapshots.

Verification

  • Change the behaviour under test slightly; the snapshot test fails with a readable diff.
  • Run the suite twice, in another time zone (TZ=Asia/Kolkata) and on CI; snapshots stay identical.
  • CI with a deleted snapshot file fails instead of regenerating it.

Gotchas

  • -u on the whole suite accepts every regression currently failing.
  • Snapshotting React trees couples tests to markup and class names; a CSS refactor rewrites hundreds of snapshots and reviewers stop reading.
  • Snapshot files merge badly; resolve conflicts by regenerating after rebasing, then review.
  • A snapshot of an error message from a third-party library breaks on its minor releases; assert on your own error type instead.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Snapshot Testing"
  short_description: "Snapshots small enough to actually review"
  default_prompt: "$snapshot-testing Our Jest snapshots change on every PR and nobody reads them, clean this up"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem