When assertions get in the way
You're refactoring a function that generates HTML. You fix a bug where the class attribute wasn't closing correctly. You run the tests. They pass. But now you notice the indentation is off by two spaces, and a new attribute appeared that you didn't expect. Writing an assertion for the entire HTML string is painful. You'd have to escape quotes, handle newlines, and update the assertion every time the design changes slightly. You want to verify the whole structure without typing assert_eq!(output, "expected...") for every case.
Snapshot testing solves this. You run the test, it saves the output. Next time, it compares. If something changes, you review the diff and decide if it's a bug or an intentional update.
What snapshot testing actually does
Snapshot testing captures the output of your code and stores it as a reference file. When the test runs again, it compares the current output against that file. If they match, the test passes. If they differ, the test fails and shows you the difference. You decide whether the change is correct.
This approach works well for complex outputs like JSON, HTML, formatted text, or large data structures where checking every field manually is tedious. It also shines when the output is stable but hard to construct in a test. You generate the output once, save it, and then your test ensures nothing drifts.
Think of it like a version-controlled screenshot of your data. You don't check every pixel manually. You look at the diff and judge the change.
Getting started with insta
The standard tool for snapshot testing in Rust is the insta crate. Add it to your dev-dependencies along with the cargo-insta binary. Snapshot testing is a testing tool. It shouldn't bloat your production binary. The cargo-insta binary provides the review workflow that makes the crate useful.
[dev-dependencies]
insta = "1.38"
cargo-insta = "1.38"
Here is a minimal test. It checks a formatted string.
use insta::assert_snapshot;
#[test]
fn test_greeting() {
let result = format!("Hello, {}!", "World");
// First run creates a .snap file with this content.
// Subsequent runs compare against the saved file.
assert_snapshot!(result);
}
Run cargo test. The test fails on the first run because no snapshot exists yet. insta prints the new snapshot and tells you to review it. This is the expected behavior. The crate forces you to acknowledge the initial state.
The review workflow
The magic of insta is the review workflow. You don't edit snapshot files manually. You use cargo insta to review changes in a terminal interface.
Run cargo insta test. This command runs your tests but captures snapshot changes instead of failing immediately. It prints a summary of pending snapshots.
insta test finished. 1 snapshot to review.
Run cargo insta review. A terminal user interface opens, showing the diff between the current output and the saved snapshot. Press a to accept the change, r to reject, or q to quit. Accepting updates the snapshot file. Rejecting keeps the old version and marks the test as failed.
This workflow lets you review multiple changes in one session. You can accept a batch of updates after a refactor, or reject a single regression while keeping the rest. It keeps your git history clean and ensures you actually look at the changes.
Convention aside: always run cargo insta review before committing. If you skip the review and just edit files manually, you lose the safety net. The review process is the guardrail.
Realistic examples
Use assert_json_snapshot! for JSON data. It normalizes the structure, sorting keys and formatting consistently. This prevents tests from failing due to key order changes, which are common in hash maps.
use insta::assert_json_snapshot;
use serde_json::json;
#[test]
fn test_api_response() {
let response = json!({
"status": "ok",
"data": { "id": 42, "name": "Widget" }
});
// Normalizes key order and formatting.
// Safe against hash map iteration order changes.
assert_json_snapshot!(response);
}
For structs that don't serialize to JSON, use assert_debug_snapshot!. It uses the Debug implementation to generate the snapshot.
use insta::assert_debug_snapshot;
#[derive(Debug)]
struct User {
id: u32,
name: String,
active: bool,
}
#[test]
fn test_user_struct() {
let user = User {
id: 1,
name: "Alice".to_string(),
active: true,
};
// Generates a snapshot from the Debug output.
// Useful for complex structs where JSON isn't available.
assert_debug_snapshot!(user);
}
Snapshots are a contract with your future self. Treat them like code.
Handling non-deterministic output
Non-deterministic output breaks snapshots. If your code includes timestamps, random IDs, or memory addresses, the output changes every run. The test fails constantly. You must stabilize the output.
Use redaction to replace dynamic parts with placeholders. insta supports inline redaction patterns. The pattern matches the dynamic part and replaces it in the snapshot.
use insta::assert_snapshot;
#[test]
fn test_with_timestamp() {
let log = "2023-10-27T10:00:00Z - Request processed";
// Redact the timestamp pattern so the snapshot stays stable.
// The pattern matches ISO timestamps and replaces them.
assert_snapshot!(log, @"____ - Request processed");
}
For more complex redactions, use the redact helper or settings. Bind settings to the scope of the test to prevent leaking redaction rules to other tests.
use insta::{assert_snapshot, settings};
#[test]
fn test_with_id_redaction() {
// Bind settings for the scope of this test.
// This prevents leaking redaction rules to other tests.
let _settings = settings();
settings.bind(|| {
settings.add_redaction(
r#""id": "[0-9]++""#,
insta::dynamic_redaction(|value, _path| format!("__{}__", value.len())),
);
});
let json = r#"{"id": "12345", "name": "Alice"}"#;
// The ID is redacted dynamically based on the pattern.
assert_snapshot!(json);
}
If your test depends on time or randomness, you haven't written a test. You've written a lottery ticket.
Inline versus file snapshots
insta supports two storage modes: file snapshots and inline snapshots. File snapshots store the reference in a .snap file next to the test file. Inline snapshots store the reference directly in the test code.
Use inline snapshots with assert_snapshot!(value, @"expected"). The @"expected" part is the reference. If the value changes, cargo insta inline updates the string in the source file. This keeps the test self-contained. Git diffs show the change right in the test file.
use insta::assert_snapshot;
#[test]
fn test_inline() {
let result = "Short output";
// Reference lives in the code.
// cargo insta inline updates this string.
assert_snapshot!(result, @"Short output");
}
Use file snapshots for large outputs. They keep test files readable and avoid bloating source code with massive strings. Use inline snapshots for short outputs where the reference belongs logically inside the test.
Convention aside: name your snapshots when you have multiple in one test. assert_snapshot!(name, value) creates a snapshot file named after the test and the name. This keeps snapshots organized.
#[test]
fn test_multiple() {
let header = "<h1>Title</h1>";
let footer = "<footer>End</footer>";
// Named snapshots create separate files.
assert_snapshot!("header", header);
assert_snapshot!("footer", footer);
}
When to use snapshots versus other assertions
Use assert_snapshot! when verifying large strings, formatted output, or complex structures where field-by-field assertions are verbose.
Use assert_json_snapshot! when testing JSON or data that serializes to JSON, because it normalizes key order and formatting to prevent false failures.
Use inline snapshots with assert_snapshot!(value, @"expected") when the expected output is short and belongs logically inside the test code for immediate visibility.
Use standard assert_eq! when you only care about a specific property, like a boolean flag or a single numeric value, rather than the entire output.
Use redaction patterns when your output contains non-deterministic data like timestamps, IDs, or memory addresses that change between runs.
Pick the tool that matches the shape of your data. Don't force a snapshot where a simple assertion does the job.