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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- snapshot-testing Installs to ~/.agents/skills/snapshot-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install snapshot-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project snapshot-testing Installs to .agents/skills/snapshot-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/snapshot-testing.zipunzip -o snapshot-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) snapshot-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You need to compare screenshots or pixels: use visual-regression-testing
- You are testing React component behaviour: use react-testing-library
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.
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
- 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. - 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. - 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
| Output | Snapshot? |
|---|---|
| Serialised structures with many fields: API responses, generated config, AST, SQL from a query builder, CLI output, error messages | Yes, ideal |
| Code generators, formatters, compilers, migrations generated from models | Yes, golden files |
| One or two key values (total, status, a flag) | No, assert explicitly |
| Whole rendered component trees with styling classes | No; test behaviour with Testing Library, appearance with visual tests |
| Output containing time, random ids, ordering of unordered collections | Only after redaction and sorting |
| Pixels | No; 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
toMatchInlineSnapshotfor 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"orjest -u -t "invoice"for the tests you changed, not the whole suite. - CI:
jest --cifails when a snapshot is missing instead of writing it; Vitest does not write new snapshots whenCIis set. Remove obsolete snapshots with-ulocally 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,.ambrand.goldendiffs as code: read every changed line. - A PR that updates snapshots says why in the description (“added
currencyfield 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
- Rank snapshot files by churn and size.
- 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).
- Remove obsolete snapshots; most runners can report them.
- 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
-uon 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.yaml
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.