Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Snapshots

A snapshot test asserts that a value still renders the way it did last time. Instead of writing the expected output by hand, you let the test record it once, commit that, and fail on any later change. It is the right tool for output that is large, structured, or tedious to spell out: rendered HTML, serialized payloads, formatted reports, error messages.

test-better has two flavours: file snapshots, stored in a .snap file next to the test, and inline snapshots, stored in a string literal in the test itself.

File snapshots

check!(value).matches_snapshot("name") compares the value’s Display output against tests/snapshots/<module_path>__<name>.snap:

#![allow(unused)]
fn main() {
use test_better::prelude::*;

#[test]
fn the_home_page_renders() -> TestResult {
    let rendered = render_home_page();
    check!(rendered).matches_snapshot("home_page")
}
}

The first time this runs there is no .snap file, so the test fails with a “missing snapshot” error. Record it by running with UPDATE_SNAPSHOTS=1:

UPDATE_SNAPSHOTS=1 cargo test

That writes the .snap file. Review it, commit it, and from then on the test compares against it. When the output legitimately changes, re-run with UPDATE_SNAPSHOTS=1 and commit the updated file; when it changes unexpectedly, the test fails with a diff.

Inline snapshots

For short values, an inline snapshot keeps the expected output in the test:

#![allow(unused)]
fn main() {
use test_better::prelude::*;

#[test]
fn arithmetic_still_works() -> TestResult {
    check!(2 + 2).matches_inline_snapshot("4")
}
}

Multi-line values are written as a raw string; leading indentation is normalized, so the literal can be indented to match the surrounding code:

#![allow(unused)]
fn main() {
use test_better::prelude::*;

#[test]
fn the_report_renders() -> TestResult {
    let report = ["name: alice", "score: 42", "status: active"].join("\n");
    check!(report).matches_inline_snapshot(
        r#"
        name: alice
        score: 42
        status: active
        "#,
    )
}
}

An inline snapshot starts empty. Run the test under UPDATE_SNAPSHOTS=1 and it records a pending patch rather than editing your source mid-run; apply the pending patches with the cargo test-better accept companion (see the runner recipe).

Redactions: ignoring the parts that always change

Real output often contains values that change every run (timestamps, UUIDs, temp paths) but are not what the test is about. Redactions rewrites those to a stable placeholder before the comparison:

#![allow(unused)]
fn main() {
use test_better::Redactions;
use test_better::prelude::*;

#[test]
fn the_audit_line_renders() -> TestResult {
    let line = format!("{} user=alice action=login", now_rfc3339());
    let redactions = Redactions::new()
        .redact_rfc3339_timestamps()
        .redact_uuids();
    check!(line).matches_snapshot_with("audit_line", &redactions)
}
}

Redactions is a builder: redact_rfc3339_timestamps and redact_uuids are built in; replace(needle, placeholder) swaps a fixed string; redact_with takes an arbitrary rewrite rule. matches_snapshot_with and matches_inline_snapshot_with take the configured Redactions.

When to snapshot, and when not

Snapshots are powerful but blunt: a snapshot test asserts on the whole output, so it fails on any change, intended or not. Use one when the output is genuinely too large or too structured to assert piece by piece. When you care about one field, a targeted check! with matches_struct! or contains_str says more about what matters and fails more precisely.