Golden-master (characterization) and snapshot testing with insta, Jest and deterministic rendering baselines
Document status:
reviewed. Canonical source.
Summary
A characterization test records the observed behaviour of existing code as its oracle so that later changes are detected; it documents actual, not desired, behaviour (Feathers). Snapshot testing is the same idea automated: the first run stores the output, later runs diff against it. Jest 14 (2016) popularised the practice for UI trees and introduced the review-and-update workflow (toMatchSnapshot, -u, --ci). The Rust crate insta provides file and inline snapshots, serialised snapshot macros, redactions and filters for nondeterministic content, a with_settings! scope, an INSTA_UPDATE policy and the cargo insta review workflow. Grey-literature studies identify fragility, lack of context, large snapshots and blind approval as the main drawbacks. Deterministic visual baselines in browsers rely on the Ahem font, per-platform baselines, disabled animations and perceptual pixel thresholds.
For NUIF, snapshots are the storage form of the canonicalization, layout and render suites: canonical text, resolved box tables and images. The evidence below determines how to keep them deterministic and reviewable.
Evidence
- Characterization testing documents “your system’s actual behavior, not check for the behavior you wish your system had”; a production system “becomes its own specification”. Feathers, https://michaelfeathers.silvrback.com/characterization-testing, retrieved 2026-08-29. Book: Working Effectively with Legacy Code, Prentice Hall 2004, ISBN 0131177052, chapter 13 (chapter text not retrieved).
- Wikipedia equates characterization test with “Golden Master Testing” and notes such tests verify observed behaviour, not correctness. https://en.wikipedia.org/wiki/Characterization_test, retrieved 2026-08-29.
- ApprovalTests: “Also known as Golden Master Tests or Snapshot Testing”;
*.approved.*files are committed,*.received.*files are transitory; a reporter opens a diff tool on failure only. https://github.com/approvals/ApprovalTests.cpp/blob/master/doc/README.md and https://github.com/approvals/ApprovalTests.Net README, retrieved 2026-08-29. - Jest 14.0 (2016-07-27) introduced
toMatchSnapshot()withreact-test-renderer, storingpretty-formatoutput in.snapfiles and updating withjest -u. https://jestjs.io/blog/2016/07/27/jest-14, retrieved 2026-08-29. - Jest docs: snapshots live in
__snapshots__/*.snap;--updateSnapshot/-ure-records failing snapshots;--cifails instead of writing new snapshots; as of Jest 20 snapshots are not written on CI without--updateSnapshot;toMatchInlineSnapshot(); property matchers such asexpect.any(Date)are checked before the snapshot is written. https://jestjs.io/docs/snapshot-testing and https://jestjs.io/docs/cli, retrieved 2026-08-29. - Jest best practices: treat snapshots as code and review them; resist regenerating snapshots instead of examining root causes (
no-large-snapshotslint); tests must be deterministic (mockDate.now()); use descriptive names. https://jestjs.io/docs/snapshot-testing §Best Practices. - insta 1.48.0 stores file snapshots as
snapshots/<module>__<name>.snapnext to the test; pending snapshots are.snap.new(inline:.pending-snap); header is YAML withsource,expression,input_file, separated from the body by---; files are normalised to LF before diffing. https://docs.rs/insta/latest/insta/, https://insta.rs/docs/snapshot-types/, https://insta.rs/docs/snapshot-files/, retrieved 2026-08-29. - Macros:
assert_snapshot!,assert_debug_snapshot!,assert_json_snapshot!,assert_compact_json_snapshot!,assert_compact_debug_snapshot!,assert_yaml_snapshot!,assert_ron_snapshot!,assert_csv_snapshot!,assert_toml_snapshot!,assert_binary_snapshot!(experimental, compared byte for byte),with_settings!,glob!; featuresyaml,json,ron,csv,toml,redactions,filters,glob. https://docs.rs/insta/latest/insta/index.html#macros, retrieved 2026-08-29. - Inline snapshots use a trailing
@"..."argument; after review the tool rewrites it to@r###"..."###. https://insta.rs/docs/quickstart/ and https://insta.rs/docs/snapshot-types/. INSTA_UPDATE:auto(default:noon CI,newotherwise),new(write.snap.newpending review),always(write.snap, bypass review),unseen(alwaysfor new,newfor existing),no(never write),force(rewrite even if passing).INSTA_FORCE_PASS=1lets tests pass to collect multiple snapshots;INSTA_OUTPUT∈ {diff, summary, minimal, none};INSTA_WORKSPACE_ROOToverrides cargo-based root detection. https://docs.rs/insta/latest/insta/ and https://insta.rs/docs/advanced/, retrieved 2026-08-29.with_settings!({sort_maps => true}, { ... });Settingssetters:set_sort_maps(“forceful sorting of maps before serialization”),set_snapshot_path(defaultsnapshots),set_prepend_module_to_snapshot,set_snapshot_suffix(parameterised tests),set_description,set_info,set_omit_expression,set_redactions,set_filters,set_strip_ansi_escape_codes,set_input_file,set_comparator. https://insta.rs/docs/settings/ and https://docs.rs/insta/latest/insta/struct.Settings.html, retrieved 2026-08-29.- Redactions (feature
redactions) are a third macro argument{ "selector" => replacement }with selectors.key,["key"],[index],[],[start:end],.*,.**; helpersinsta::dynamic_redaction(|value, path| ...),insta::sorted_redaction()for unordered collections andinsta::rounded_redaction(3)for floats. https://insta.rs/docs/redactions/, retrieved 2026-08-29. - Filters (feature
filters) are regex replacements applied to the string form, e.g.(r"\b[[:xdigit:]]{32}\b", "[UID]"), for content that is inherently textual. https://insta.rs/docs/filters/, retrieved 2026-08-29. cargo instasubcommands:review(aliasverify),accept,reject,testwith--review,--accept,--accept-unseen,--check,--force-update-snapshots, and--unreferenced∈ {ignore, warn, reject, delete, auto};pending-snapshots;show. https://insta.rs/docs/cli/ andcargo-insta/src/cli.rsonmaster, retrieved 2026-08-29.- YAML is the recommended serialiser “because YAML is human readable and excellent at diffing because it is line based”. https://insta.rs/docs/serializers/, retrieved 2026-08-29.
- Pitfalls: a grey-literature review of 50 documents finds fragility (28%), lack of context (22%), large snapshots (16%), manual verification (12%) and flakiness (6%); “blindly updating the test results” is the named failure mode; mitigations are code review (26%), treating snapshots as code (22%) and small snapshots (14%). Cruz, Rocha, Valente, “Snapshot testing in practice: Benefits and drawbacks”, JSS 204 (2023) 111797, DOI 10.1016/j.jss.2023.111797, §2, §4.2 Table 3, §4.3 Table 4 (PDF https://homepages.dcc.ufmg.br/~mtov/pub/2023-jss-snapshot.pdf, retrieved 2026-08-29).
- Dodds (2017) quotes Searls: developers “will sooner just nuke the snapshot and record a fresh passing one”, and states that snapshots beyond a few dozen lines suffer maintenance issues. https://kentcdodds.com/blog/effective-snapshot-testing, retrieved 2026-08-29.
- Playwright: screenshots differ across browsers and platforms “due to different rendering, fonts and more” and must be generated in the same environment; file names carry browser and platform suffixes;
toHaveScreenshotdefaultsanimations: "disabled",caret: "hide",scale: "css",threshold0.2 (YIQ perceived colour difference), optionalmaxDiffPixels,maxDiffPixelRatio,mask,stylePath. https://playwright.dev/docs/test-snapshots and https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-1, retrieved 2026-08-29. - pixelmatch v5.3.0
threshold0.1 default using YIQ colour difference (Kotsarenko and Ramos 2010) with anti-aliasing detection; the current README cites OKLab instead. https://github.com/mapbox/pixelmatch/blob/v5.3.0/README.md and https://github.com/mapbox/pixelmatch, retrieved 2026-08-29. - WPT reftest fuzzy matching:
<meta name="fuzzy" content="maxDifference=15;totalPixels=300">with inclusive ranges and per-reference prefixes. https://web-platform-tests.org/writing-tests/reftests.html, retrieved 2026-08-29. - Ahem font: “well defined glyphs of precise sizes and shapes”; em-square exactly square; baseline 0.2em above bottom; X is a 1em square, p a 0.2em rectangle below baseline, É a 0.8em rectangle above, space transparent. https://web-platform-tests.org/writing-tests/ahem.html, retrieved 2026-08-29. Chromium: “Use the Ahem font to reduce the variance introduced by the platform’s text rendering system”; pixel baselines are
-expected.pngwithplatform/<PLATFORM-VERSION>fallback chains. https://chromium.googlesource.com/chromium/src/+/main/docs/testing/writing_web_tests.md and web_test_baseline_fallback.md, retrieved 2026-08-29. - expect-test 1.5.1 offers
expect![[...]]inline snapshots updated withUPDATE_EXPECT=1and lists insta as the more complete alternative. https://docs.rs/expect-test, retrieved 2026-08-29.
Mechanism
Snapshot assertion lifecycle (insta):
assert_*_snapshot!(name?, value, redactions?):
text = serialize(value, format) # yaml/json/ron/csv/toml/debug/display
text = apply_redactions(text, selectors) # structured, before serialization
text = apply_filters(text, regexes) # string level
path = snapshot_path / (module__name[suffix]).snap
if exists(path) and body(path) == text: pass
else match INSTA_UPDATE:
no -> fail
new -> write path.snap.new; fail (pending review)
always -> write path.snap; pass
unseen -> exists ? new : always
force -> write path.snap regardless
review: cargo insta review walks *.snap.new, shows diff, accept/reject moves or deletes
Determinism checklist for NUIF snapshots, with attribution:
- Stable key ordering:
BTreeMapinnuif_core::Documentalready sorts; usesort_mapsfor anyHashMap(insta settings). - Float normalisation:
rounded_redaction(n)or, preferably, round in the canonical encoder so that the snapshot equals the canonical text (insta redactions;spec/08-serialization.mdnumeric normalisation). - Redact or fix generated identifiers and timestamps; NUIF should instead generate IDs from the seed so no redaction is needed (Jest property matchers; JSS D14).
- Pin fonts: ship Ahem or an equivalent metric-defined font for text fixtures (WPT Ahem; Chromium; Taffy and Yoga fixtures embed Ahem).
- Fix viewport, scale factor, writing direction, locale and theme through
nuif_layout::EvaluationContext(Playwrightscale,deviceScaleFactor). - No animations or carets in rendered snapshots (Playwright defaults).
- Per-platform image baselines only where the rasteriser is platform dependent; prefer a CPU reference path so one baseline suffices (Chromium fallback chains as the case to avoid).
- Perceptual tolerance declared per fixture (
maxDifference;totalPixels, YIQ/OKLab threshold), never global (WPT fuzzy; pixelmatch). - Never write snapshots on CI (
INSTA_UPDATE=no, Jest--ci). - Keep snapshots small and named; review in code review; prune with
--unreferenced=delete(Jest best practices; JSS Table 4; cargo-insta).
NUIF relevance
Borrow
- insta file snapshots in YAML for canonical documents, resolved box tables and fidelity reports, with
with_settings!suffixes for the 360/768/1440 viewport matrix (instaset_snapshot_suffix,sort_maps). - The
INSTA_UPDATE=noon CI pluscargo insta reviewworkflow as the approval gate for the golden structural fixtures named inconformance/PLAN.md. - Ahem-style metric fonts for text fixtures and WPT-style
maxDifference;totalPixelsfuzzy declarations for render fixtures where exact pixels are not normative. - Snapshot header metadata (
description,info) to carry implementation version, capability profile, fixture ID and evaluation context as required byconformance/PLAN.md.
Adapt
- Redactions should be unnecessary for canonical NUIF output; if a snapshot needs redaction, the encoder or the seed handling is nondeterministic and should be fixed instead.
- Image snapshots must record the comparison policy (metric, threshold, pixel budget) in the machine-readable report, not only in test code.
- Snapshot churn is a signal: the report should count snapshot updates per change so that over-approval is measurable (JSS 2023 fragility finding).
Reject
- Large whole-document snapshots of rendered DOM or HTML export output; export tests should snapshot the fidelity report and a normalised structural view, not raw formatter output.
- Platform-specific baseline fallback chains; NUIF should require a deterministic CPU reference renderer instead.
Open questions
- Which of the canonical text profile or YAML
Debugoutput should be the snapshot body, given that the canonical text is itself the normative artefact? - Can
insta::Settings::set_comparatorhost a tolerance-aware comparator for box tables so that resolved layouts are snapshotted with declared epsilon rather than exact text? - How should image snapshots be stored in the repository without bloating history: content-addressed assets in the
.nuifpackage or Git LFS?