Snapshot Testing Rust Code with cargo-insta
I discovered cargo-insta while working on a pull request for cargo-mutants and it immediately clicked for me.
It solves a very real problem I kept running into: how do you test complex output (JSON, diagnostics, CLI output) without filling your test files with walls of strings or hand-rolled comparison logic?
This post introduces snapshot testing in Rust with
insta
and walks through a complete, copy-pastable example using the cargo-insta CLI. By the end, you should be able to:
- add
instato a new crate - write and run snapshot tests
- understand where snapshots live and how to review changes
Why snapshot testing?
Traditional assertions like
assert_eq!(result, "some expected string");
are great when:
- the expected value is short, and
- it rarely changes.
They become painful when:
- the output is large (think multi-line JSON)
- the structure changes often
- you want to see a nice diff when something breaks
Snapshot testing flips the mental model:
- You run the code once, capture the output, and store it as a snapshot file.
- Future test runs compare the current output against that snapshot.
- When output changes, you review the diff and either:
- accept the new snapshot (change is intentional), or
- fix the code (change is a regression).
The snapshot becomes the “golden master” of what you expect the output to look like.
Getting started with insta and cargo-insta
insta is the Rust crate that does the heavy lifting: macros that record and compare snapshots in your tests.
For this example we’ll use JSON snapshots, which work nicely for tools that emit structured output (reports, diagnostics, etc.).
Add the dependencies:
[dependencies]
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
insta = { version = "1", features = ["json"] }
The crate provides several macros, for example:
assert_snapshot!for string snapshotsassert_debug_snapshot!forDebugoutputassert_json_snapshot!for JSON-serializable values- plus others for YAML, TOML, CSV, etc.
To make the workflow nice, there is also a CLI tool: cargo-insta.
Install it like any other cargo subcommand:
cargo install cargo-insta
You’ll then have commands such as:
cargo insta test— run tests and collect snapshot changescargo insta review— interactively review diffs and accept or reject changescargo insta test --review— run tests and immediately open the review UI
Now let’s put all of this together in a small, fully working project.
A complete, working example
We’ll build a tiny crate that produces a “mutation report” and snapshot-test its JSON output.
If you create the following files, you will have a fully working example.
1. Project layout
insta-demo/
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── snapshot_report.rs
From scratch:
cargo new insta-demo --lib
cd insta-demo
2. Cargo.toml
Replace the contents of Cargo.toml with:
[package]
name = "insta-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
insta = { version = "1", features = ["json"] }
# Optional but nice: faster, smaller insta in dev builds
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3
The profile.dev.package bit is optional but recommended by the insta docs: it makes diffs faster and keeps memory usage down.
3. Library code: src/lib.rs
This is the code we want to test:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct MutationReport {
pub mutants: u32,
pub killed: u32,
pub timeout: u32,
}
pub fn sample_report() -> MutationReport {
MutationReport {
mutants: 42,
killed: 39,
timeout: 1,
}
}
It’s intentionally simple, but realistic enough: a struct you might serialize to JSON in a testing tool.
4. Snapshot test: tests/snapshot_report.rs
Now create the test file:
use insta::assert_json_snapshot;
use insta_demo::sample_report;
#[test]
fn mutation_report_snapshot() {
let report = sample_report();
assert_json_snapshot!("mutation_report", report);
}
A few details:
insta_demois the crate name, derived fromname = "insta-demo"inCargo.toml.assert_json_snapshot!takes a snapshot name ("mutation_report") and a value that implementsserde::Serialize.- insta will serialize the value to JSON and manage snapshot files for you.
First run: creating the initial snapshot
The recommended flow is:
- Run the tests once.
- Look at what insta produced.
- If you’re happy with it, accept the snapshot.
Start with:
cargo test
On this first run:
- The test will fail because there is no accepted snapshot yet.
- insta writes a proposed snapshot next to your tests, with a
.snap.newextension.
In this example you’ll see something like:
tests/snapshots/snapshot_report__mutation_report.snap.new
Now review and accept it:
cargo insta review
This opens an interactive review UI where you can see the new snapshot. If it looks correct, accept it. insta will rename:
snapshot_report__mutation_report.snap.new
→ snapshot_report__mutation_report.snap
Now you have a baseline snapshot committed to your repo.
At this point:
cargo test
should pass cleanly:
- insta finds
*.snapfiles - compares your current output against them
- and everything matches
Tip: once you’re comfortable, you can also use:
cargo insta test --review
which runs your tests and then directly opens the review UI in one go.
Changing code and seeing diffs
Now let’s simulate a change or regression.
Modify sample_report in src/lib.rs:
pub fn sample_report() -> MutationReport {
MutationReport {
mutants: 42,
killed: 39,
timeout: 2, // changed from 1 to 2
}
}
Using cargo test (CI-style)
Run:
cargo test
Because there is an accepted snapshot (.snap) and the output has changed, the test will fail.
You’ll see a snapshot summary, a diff of the old vs new JSON, and a hint to run cargo insta review if the change is intentional.
This is typically how you want things to behave in CI: if snapshots are out of date, the build goes red.
Using cargo insta test (local workflow)
For local development you can also do:
cargo insta test
With the default settings (INSTA_UPDATE=auto, and no CI=true), insta will:
- run all tests
- notice the changed output
- write a new
*.snap.newfile next to the existing*.snap - keep the test run green, but tell you there are snapshots to review
Then:
cargo insta review
lets you inspect the diff and decide:
- Accept → the
.snap.newreplaces the old.snapand becomes the new baseline. - Reject → keep the old snapshot and fix your code instead.
A common local loop is:
Change code →
cargo insta test→cargo insta review→ commit code + updated.snapfiles.
Using insta and cargo-insta in CI
In CI you normally don’t want new snapshots to be written automatically.
By default insta looks at the CI environment variable. When CI=true:
- insta does not write new snapshot files, even with
INSTA_UPDATE=auto - mismatches simply make your tests fail
A typical setup is:
# In CI
export CI=true
cargo test
Locally, you then fix things with:
cargo insta test
cargo insta review
and commit the updated *.snap files.
If you ever want more control, you can use the INSTA_UPDATE environment variable (for example, INSTA_UPDATE=no to never write snapshots, INSTA_UPDATE=always to overwrite, etc.), but for most projects the default behaviour plus cargo insta review is enough.
How this fits into the Mutorium universe
At Mutorium Labs we care a lot about:
- Testing (obviously)
- developer tooling
- making tests that actually catch real bugs (hello, mutation testing)
Snapshot testing with insta and cargo-insta fits in nicely:
- Tools like
cargo-mutants,noir-metrics, andzk-mutantproduce structured output (JSON, reports, diagnostics). - Snapshot tests are a lightweight way to assert “this is what the output should look like” without manually writing huge strings in your test files.
- Once you have a nice suite of snapshot tests, you can point a mutation testing tool at the code and ask:
- “If I subtly break this logic, do my snapshot tests notice?”
Snapshot tests give you broad coverage of outputs. Mutation testing then checks whether those tests are sensitive to real faults. Together, they make regressions much harder to sneak in.
Beyond this example
This post focused on a small, JSON-based example to get you going. insta can do a lot more:
- Snapshot formats:
- YAML (the format the insta docs prefer),
- JSON, TOML, CSV, RON…
- Inline snapshots that live directly in your test code and are updated by
cargo insta review. - Annotations via
with_settings!to include extra context (like template source or input data) in the snapshot, which really helps during review. - Fine-grained control over updates using the
INSTA_UPDATEenvironment variable. - Behaviour tuned for CI, where new snapshots are never written automatically.
If this example clicked for you, it’s worth browsing the insta documentation to see what else is possible.
Summary
To recap:
- Snapshot testing is a powerful way to test complex output like JSON and text without huge inline strings.
instaprovides the snapshot macros (assert_json_snapshot!,assert_debug_snapshot!,assert_snapshot!, …).cargo-instaadds a great workflow:cargo testto behave normally and fail when snapshots don’t matchcargo insta testto collect snapshot changescargo insta reviewto interactively inspect and accept/reject diffs
- With the
insta-demoexample above, you now have a complete, minimal setup you can copy into your own projects.
If you haven’t tried snapshot testing in Rust yet, cargo-insta is a great place to start. And if you’re already using mutation testing, combining the two can give you a very powerful safety net.
Happy snapshotting!