Announced on July 15 at RubyConf 2026 in Las Vegas.
Today at RubyConf 2026, I’m excited to release a project that fell out of building Herb:

The lightning talk page, and the recording once it’s available, lives on RubyEvents.
📸 Insta, snapshot testing for Ruby, with an interactive review workflow. Think of it as assert_equal, where the expected value manages itself.
Insta is extracted from Herb’s test suite, where a hand-rolled SnapshotUtils module grew past five hundred lines keeping thousands of parser and compiler tests honest. And it’s a love letter to insta, Armin Ronacher’s snapshot testing library for Rust, whose interactive review workflow this gem brings to Ruby.
gem "insta"
That afternoon
You add one field to a serializer, avatar_url, and now thirty assertions across your suite need the same hand-edit. Or you change a price calculation, or some copy. We’ve all had that afternoon.
class UserSerializerTest < Minitest::Test def test_user_json expected = '{"id":42,"name":"Ada","role":"admin","created_at":"2026-01-15T09:30:00Z"}' assert_equal expected, UserSerializer.render(user) end # ...and 28 more of theseend
There are two classic ways to write this test, and both hurt. The first is the one above: a hand-maintained expected value, usually copy-pasted from the failure output of the test you just wrote. When the output changes on purpose, you get a wall-of-text diff and you update it by copy-pasting from the failure output again:

The second is the “proper” version: actually asserting the structure. For one small parsed template in Herb’s test suite, that’s dozens of assertions navigating the syntax tree by hand, and it still doesn’t check source locations. For thousands of parser trees, lexer outputs, and compiled templates, neither version was ever an option, which is why Herb ended up with its own hand-rolled snapshot module in the first place.
So here’s the question: what if your testing tool updated those values for you, everywhere, and your only job was to look at each change and decide, accept or reject?
The idea
Snapshot tests assert values against a reference snapshot that Insta manages for you:
def test_user_json assert_snapshot(UserSerializer.render(user))end
No expected value. On the first run, the assertion itself writes the captured output to a pending .snap.new file inside your test/snapshots/ folder, right at assertion time. Once accepted, it becomes the .snap file your tests assert against:
# test/snapshots/user_serializer_test/test_user_json.snap---source: UserSerializerTest#test_user_json---{"id":42,"name":"Ada","role":"admin","created_at":"2026-01-15T09:30:00Z"}
Here is what the first run looks like:


You review it, approve it, and from then on it’s a regular assertion: output changes, test fails.
Insta shines when you want to assert the whole thing, without writing an assertion for every detail: full JSON API responses, rendered views and HTML, ASTs and compiled output, CLI output, error messages. See that it changed, see how, then accept or reject.
The review workflow
This is the part that changes how testing feels, and it’s the idea I fell in love with in Rust’s insta. When output changes, you don’t read a wall of text in a test failure. You run:

For every changed snapshot you get your test’s source with an arrow at the assertion, a syntax-aware structural diff powered by difftastic, and one decision:

Two properties make this workflow feel great in practice:
Review never re-runs your tests. Pending snapshots are written at assertion time during the test run, so insta review and insta accept are pure file operations, instant even on a large suite.
Cascades collapse. When one logic change touches ten snapshots, that’s one review session, not ten hand-edits. That afternoon from the beginning is five minutes now. Your test suite becomes a conversation: here’s what changed, is this what you meant?
Inline snapshots: the test that writes itself
For small values you don’t want a separate file:
def test_reverse assert_inline_snapshot("Hello World".reverse)end
Run it, review, accept, and Insta edits your test file:
def test_reverse assert_inline_snapshot("Hello World".reverse, "dlroW olleH")end

The value is deliberately one nobody wants to type by hand. Multi-line values become heredocs automatically. And the machinery that finds that exact call site and rewrites it safely? A Prism visitor. Prism is everywhere.
Serializers, redactions, and volatile values
Snapshots aren’t limited to strings. Pass a serializer to capture structured values, one snapshot where you’d otherwise write many assertions:
def test_user_payload user = { id: 42, name: "Ada", roles: [:admin, :author], active: true } assert_snapshot(user, serializer: :yaml, name: "user_payload")end
Volatile values like IDs and timestamps would make snapshots fail on every run, so you can redact them with jq-like selectors:
assert_snapshot( user, serializer: :yaml, redact: { ".id" => "[id]", ".**.created_at" => "[timestamp]" })
For volatile values embedded in plain strings, freeze time in the test (travel_to or Timecop), which composes naturally with snapshots.
Why this matters more in 2026
In the agent era, most of us review more code than we write. And when an agent trips a hand-written assertion, its favorite move is editing the expected value until the test passes, laundering a behavior change through your test suite.
Snapshots split those roles cleanly: the agent changes the code, the diff shows the behavioral delta, and accepting stays a separate, human, auditable step. git diff on your .snap files shows exactly what was approved. In case we are all going to end up as “Software Reviewers”, our tests might as well be built for it.
Where this came from
I mentioned Insta is extracted from Herb and inspired by Rust. Here’s the full journey, because it matters.
I first met snapshot testing in the React world, and Herb’s own JavaScript packages carry over a hundred Vitest snapshots today, across the linter, formatter, and language server suites. The Ruby side of Herb got a hand-rolled module called SnapshotUtils, with domain assertions like assert_parsed_snapshot and assert_compiled_snapshot. It’s over five hundred lines on main today, used across a hundred test files. It worked. But I knew it was a homegrown version of something better.
Then, building Herb’s Rust bindings, I needed the same thing a third time, and that’s where I discovered Armin’s insta. It’s in Herb’s Cargo.toml right now. Insta had the piece the others were missing: the review workflow.
One project, three ecosystems, three snapshot tools, and the best one wasn’t the Ruby one. So I did the thing I keep telling people to do: look beyond your own ecosystem, understand why it works, and bring it back the Ruby way. I leveled up SnapshotUtils, extracted it into a gem, and took the review flow, the look and the feel, straight from insta.
Porting Herb’s test suite onto the gem deletes hundreds of lines of bespoke snapshot plumbing. Extraction as a side effect of real needs, which is how most of my favorite tools have happened.
What ships in this first release?
Everything above ships today, in the gem’s first release:
- File snapshots (
.snapwith YAML frontmatter and metadata) and pending snapshots (.snap.new) - The interactive review CLI:
review,accept,reject,pending,status,clean - Inline snapshots with Prism-powered file rewriting
- Serializers (
:to_s,:inspect,:json,:yaml) and named snapshots - Redactions with jq-like selectors
- difftastic-powered structural diffs, side-by-side or inline
- Minitest and RSpec support
INSTA_UPDATEmodes and CI auto-detection (on CI, Insta refuses to write snapshots and fails instead)
On the bench for future releases: regex filters on top of redactions, editor integration for accepting snapshots from a code action, obsolete-snapshot detection, data-driven glob fixtures, and my favorite: HTML-aware snapshots powered by Herb, normalizing and diffing the DOM instead of the string.
Insta is not a replacement for WebMock or VCR: those manage what goes into your code, Insta asserts what comes out. They compose well.
Prior art
Snapshot testing has a long lineage, and Insta stands on a lot of shoulders: approval testing and golden-master testing formalized the pattern long ago (in Ruby, the approvals gem carries that tradition), Jest popularized the term for JavaScript, Vitest refined it, and rspec-snapshot and snapshot_testing brought snapshot matchers to Ruby frameworks.
What Insta adds is the combination: framework-agnostic, centered on the interactive review CLI, structural diffs, inline rewriting via Prism, redactions, and CI-aware modes.
Thank you
- Armin Ronacher, for insta and the review workflow this gem takes its name, flow, look, and feel from.
- Renan Garcia, who generously transferred the
instagem name on RubyGems. The previous gem lives on atrenan-garcia/insta.
Try it
bundle add insta
Docs and source at github.com/marcoroth/insta-ruby. If something doesn’t behave the way you expect, please open an issue. Feedback is more than welcome.
Happy Testing!

Code blocks highlighted by Torchlight.