insta: Snapshot Testing for Rust with assert_snapshot!

insta: Snapshot Testing for Rust with assert_snapshot!

insta captures the output of your code as a snapshot file. On future runs, it compares actual output against the saved snapshot and fails if they differ. When output changes intentionally, you review and approve the new snapshot with cargo insta review. It's the right tool when manually writing expected values would be tedious, error-prone, or unmaintainable.

Key Takeaways

  • assert_snapshot! captures any Display/Debug output as a named snapshot file
  • assert_json_snapshot! and assert_yaml_snapshot! serialize structured data
  • cargo insta review opens an interactive diff for approving snapshot updates
  • Snapshot files live next to tests in snapshots/ — commit them to version control
  • INSTA_UPDATE=always lets CI re-generate snapshots automatically

What snapshot testing solves

Some test assertions are painful to write by hand. Imagine testing a code formatter that produces 40 lines of output, an API response with 15 fields, or a compiler error message. Writing assert_eq!(output, "line 1\nline 2\n...") is tedious and fragile to maintain.

Snapshot tests solve this by capturing the output the first time and comparing against it on subsequent runs. You review the initial snapshot once, commit it, and future tests catch regressions automatically.

Installation

[dev-dependencies]
insta = { version = "1", features = ["json", "yaml"] }

# Optional: install the CLI
# cargo install cargo-insta

Enable the json feature for assert_json_snapshot! and yaml for assert_yaml_snapshot!.

First snapshot

fn format_address(street: &str, city: &str, zip: &str) -> String {
    format!("{}\n{}, {}", street, city, zip)
}

#[test]
fn test_address_format() {
    let output = format_address("123 Main St", "Springfield", "12345");
    insta::assert_snapshot!(output);
}

Run this test the first time:

cargo test

The test fails with: snapshot not found. Insta creates a pending snapshot in src/snapshots/my_crate__test_address_format.snap.new.

Review and approve it:

cargo insta review

This opens an interactive interface showing the new snapshot. Press a to accept. The .snap.new file becomes .snap:

---
source: src/lib.rs
expression: output
---
123 Main St
Springfield, 12345

Commit the .snap file. Future test runs compare against it.

Named snapshots

Give snapshots explicit names to distinguish multiple snapshots in one test:

#[test]
fn test_various_addresses() {
    insta::assert_snapshot!("us_address", format_address("123 Main St", "New York", "10001"));
    insta::assert_snapshot!("uk_address", format_address("10 Downing St", "London", "SW1A 2AA"));
}

Named snapshots are stored as separate files: ...__us_address.snap and ...__uk_address.snap.

JSON snapshots

For structured data, assert_json_snapshot! serializes to JSON and diffs against the stored value:

use serde::Serialize;

#[derive(Serialize)]
struct ApiResponse {
    users: Vec<User>,
    total: usize,
    page: usize,
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

#[test]
fn test_user_list_response() {
    let response = ApiResponse {
        users: vec![
            User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
            User { id: 2, name: "Bob".into(), email: "bob@example.com".into() },
        ],
        total: 2,
        page: 1,
    };

    insta::assert_json_snapshot!(response);
}

The snapshot file stores formatted JSON:

{
  "users": [
    {
      "id": 1,
      "name": "Alice",
      "email": "alice@example.com"
    },
    {
      "id": 2,
      "name": "Bob",
      "email": "bob@example.com"
    }
  ],
  "total": 2,
  "page": 1
}

When a field changes, the diff in cargo insta review shows exactly what changed — much clearer than a line-by-line string comparison.

Redacting dynamic values

Timestamps, IDs, and random values change between runs and would make snapshots fail constantly. Redact them:

#[test]
fn test_created_user_response() {
    let response = create_user("Carol");

    insta::assert_json_snapshot!(response, {
        ".id" => "[id]",
        ".created_at" => "[timestamp]",
        ".updated_at" => "[timestamp]",
    });
}

The snapshot stores "[id]" and "[timestamp]" in those positions instead of the actual values. The test passes regardless of what the real ID or timestamp is, but still catches changes to all other fields.

Redaction supports JSONPath-like selectors:

  • .field_name — specific field
  • .[*].field_name — field in all array elements
  • .** — all values recursively

Inline snapshots

For small outputs, embed the snapshot directly in the test instead of a separate file:

#[test]
fn test_greeting() {
    let msg = greet("World");
    insta::assert_snapshot!(msg, @"Hello, World!");
}

The @"..." syntax stores the expected value in-place. When you run cargo insta review on a changed inline snapshot, it updates the source file directly.

Inline snapshots are useful for simple values where a separate file feels like overkill.

The review workflow

# Run tests — new or changed snapshots become .snap.new files
cargo <span class="hljs-built_in">test

<span class="hljs-comment"># Review all pending snapshots
cargo insta review

<span class="hljs-comment"># In the review UI:
<span class="hljs-comment"># a — accept the new snapshot
<span class="hljs-comment"># r — reject (keep the old one)
<span class="hljs-comment"># s — skip for now

<span class="hljs-comment"># Accept all pending snapshots without interactive review
cargo insta accept

<span class="hljs-comment"># Reject all pending snapshots
cargo insta reject

Accepted snapshots replace the old .snap files. Commit both the test code and the updated snapshot files together.

CI integration

In CI, you want tests to fail if snapshots don't match — never silently accept new ones. The default behavior does this.

To regenerate snapshots in CI (e.g., for a "update snapshots" job):

INSTA_UPDATE=always cargo test

This writes new snapshots without requiring interactive review. Useful in a separate CI job that creates a PR with snapshot updates.

For a stricter setup that fails on any snapshot difference (including new ones not yet reviewed):

INSTA_UPDATE=no cargo test

Snapshot file organization

By default, snapshots live in a snapshots/ directory next to the test file:

src/
  lib.rs
  snapshots/
    my_crate__test_address_format.snap
    my_crate__test_user_list_response.snap

You can configure the location in Cargo.toml:

[package.metadata.insta]
snapshot-path = "tests/snapshots"

When to use insta

Snapshot testing is a good fit when:

  • Output is large (formatted text, complex JSON, HTML, SQL)
  • Output is derived from many inputs and hard to compute by hand
  • You want to lock in behavior and be notified of any change
  • The "correct" output is easier to verify visually than to specify in advance

It's less appropriate when:

  • The test has a simple, obvious expected value (assert_eq!(2 + 2, 4))
  • Output includes high-cardinality dynamic data that can't be meaningfully redacted
  • You're testing behavior rather than output format

Practical example: testing a code formatter

fn format_sql(query: &str) -> String {
    // ... SQL formatter logic
}

#[test]
fn test_formats_select() {
    let input = "SELECT id,name,email FROM users WHERE active=true ORDER BY name";
    insta::assert_snapshot!("select_formatted", format_sql(input));
}

#[test]
fn test_formats_join() {
    let input = "SELECT u.name,o.total FROM users u JOIN orders o ON u.id=o.user_id";
    insta::assert_snapshot!("join_formatted", format_sql(input));
}

When you improve the formatter, cargo test shows exactly what changed in each snapshot. You review the diffs, accept the improvements, and commit. The entire change history is visible in version control.

Summary

insta takes snapshot testing from a JavaScript ecosystem concept and makes it idiomatic in Rust. The assert_snapshot! macros capture any serializable output, cargo insta review makes approvals interactive and clear, and redaction handles dynamic values cleanly. For tests where specifying expected output by hand is impractical, insta replaces fragile string comparisons with readable, reviewable snapshot files.

Read more