10LOC
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

  • formatInvoice is 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.snap containing 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 .snap file; bun test --update-snapshots doesn'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.