Bunintermediate
Snapshot testing with bun:test in 10 lines
Published October 23, 2026
import { test, expect } from "bun:test";
const formatInvoice = (id: number, cents: number) => ({
id,
total: `$${(cents / 100).toFixed(2)}`,
issuedAt: "2026-01-01",
});
test("formats an invoice for display", () => {
expect(formatInvoice(42, 1999)).toMatchSnapshot();
});What
expect(value).toMatchSnapshot() serializes value and, on the first run, writes it to a
__snapshots__/*.snap file next to the test. Every run after that compares the current value
against the saved one and fails the test if it changed.
Why it matters
Writing an assertion for every field of a moderately shaped object gets tedious fast, and it's
easy to under-assert — checking id and total but missing that issuedAt silently changed
format. A snapshot captures the whole shape in one line, so any change to the output — intended
or not — shows up as a diff you have to consciously accept (bun test --update-snapshots)
rather than one that slips through because nobody wrote an assertion for that particular field.
How it works
formatInvoiceis a plain function with no test-specific code in it — snapshot testing wraps around ordinary code, it doesn't require designing the code differently.toMatchSnapshot()on first run creates__snapshots__/snippet.test.ts.snapcontaining the serialized object; there's nothing to write by hand.- On every subsequent run, Bun re-serializes the current return value and diffs it against what's on disk. A mismatch fails the test and prints the diff.
- Snapshot files are meant to be committed to git — a snapshot diff in a pull request is the actual code review signal for "this output changed."
Gotchas
- Non-deterministic fields (timestamps, random IDs) break snapshots on every run unless
normalized first — either pass a property matcher (
expect(obj).toMatchSnapshot({ id: expect.any(Number) })) or strip the field before asserting. - A snapshot passing doesn't mean the output is correct — it means the output is unchanged. The first run's value has to be right before you trust anything after it.
- Deleting or renaming a test leaves an orphaned entry in the
.snapfile;bun test --update-snapshotsdoesn't prune those automatically.
Related
toMatchInlineSnapshot()— the same idea, but Bun writes the snapshot directly into the test file instead of a separate__snapshots__directory, better for small values reviewed in place.- Plain
expect(value).toEqual(...)is still the better choice when there's one or two fields that actually matter — reach for a snapshot when the whole shape is the point.