Snapshot Testing in Go: Cupaloy and go-snaps Compared
Go's testing package has no built-in snapshot testing, but two libraries fill the gap: cupaloy (simpler, widely used) and go-snaps (more features, testify integration). Both capture test output to files, compare on future runs, and update via an environment variable flag. This guide covers both with practical examples.
Key Takeaways
Cupaloy is simpler; go-snaps has more features. Cupaloy is a thin wrapper: c.SnapshotT(t, value). go-snaps adds MatchSnapshot(), testify integration, and custom serializers. Start with cupaloy; move to go-snaps if you need more.
Update snapshots with an environment variable, not a flag. Both libraries use UPDATE_SNAPS=true go test ./... (go-snaps) or UPDATE_SNAPSHOTS=true (cupaloy) instead of a CLI flag.
Snapshot files are plain text — commit them. Both libraries create text files in __snapshots__/ directories. They're human-readable and belong in source control.
Serialize to a deterministic format before snapshotting. json.Marshal in Go produces stable JSON only if your struct doesn't contain maps (maps are unordered). Use encoding/json with sorted keys or json.MarshalIndent.
Use table-driven tests with snapshots for complex inputs. Go's idiomatic table-driven tests combine naturally with snapshot testing — each test case gets its own snapshot entry.
Why Snapshot Testing in Go?
Go's testing philosophy favors explicit assertions: if got != want { t.Errorf(...) }. That works great for simple values. It breaks down for complex outputs — JSON API responses, formatted reports, code generator output — where writing out the expected value manually is tedious and brittle.
Snapshot testing solves this: capture the output once, store it, fail if it changes.
Cupaloy Setup
go get github.com/bradleyjkemp/cupaloy/v2Cupaloy stores snapshots in .snapshots/ by default. Configure via cupaloy.New() or environment variables.
Basic Usage
// orders_test.go
package orders_test
import (
"encoding/json"
"testing"
"github.com/bradleyjkemp/cupaloy/v2"
"myapp/orders"
)
var snapshotter = cupaloy.New(
cupaloy.SnapshotFileExtension(".txt"),
cupaloy.SnapshotSubdirectory("testdata/snapshots"),
)
func TestFormatOrderSummary(t *testing.T) {
order := orders.Order{
ID: "ORD-001",
Customer: "Alice",
Items: []orders.Item{
{Product: "Widget", Qty: 2, Price: 14.99},
},
Total: 29.98,
}
summary := orders.FormatSummary(order)
snapshotter.SnapshotT(t, summary)
}First run creates testdata/snapshots/TestFormatOrderSummary.txt and fails — you must review the file and approve by running:
UPDATE_SNAPSHOTS=true go <span class="hljs-built_in">test ./...Wait — that creates AND approves. For cupaloy, the workflow is:
- Run test — it fails with "snapshot not found"
- The received output is printed in the test failure
- Copy the output to the snapshot file manually, or use
UPDATE_SNAPSHOTS=true - Re-run — test passes
Cupaloy with JSON Output
For structured data, marshal to JSON before snapshotting:
func TestOrderAsJSON(t *testing.T) {
order := buildTestOrder()
b, err := json.MarshalIndent(order, "", " ")
if err != nil {
t.Fatal(err)
}
snapshotter.SnapshotT(t, string(b))
}Cupaloy Table-Driven Tests
func TestPricingRules(t *testing.T) {
tests := []struct {
name string
customer string
qty int
}{
{"standard-small", "standard", 1},
{"standard-bulk", "standard", 100},
{"premium-small", "premium", 1},
{"premium-bulk", "premium", 100},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := CalculatePrice(tc.customer, tc.qty)
b, _ := json.MarshalIndent(result, "", " ")
snapshotter.SnapshotT(t, string(b))
// Creates: testdata/snapshots/TestPricingRules/standard-small.txt
})
}
}Each sub-test gets its own snapshot file in a subdirectory.
go-snaps Setup
go get github.com/gkampitakis/go-snapsgo-snaps has a richer API and integrates with testify's assert and require:
import snaps "github.com/gkampitakis/go-snaps/snaps"Basic Usage
func TestFormatReport(t *testing.T) {
report := GenerateMonthlyReport("2024-01")
b, _ := json.MarshalIndent(report, "", " ")
snaps.MatchSnapshot(t, string(b))
}go-snaps creates __snapshots__/TestFormatReport.snap:
[TestFormatReport - 1]
{
"month": "2024-01",
"totalOrders": 142,
"totalRevenue": 15984.32
}
---Updating go-snaps Snapshots
UPDATE_SNAPS=true go <span class="hljs-built_in">test ./...go-snaps with testify
import (
"testing"
"github.com/stretchr/testify/assert"
snaps "github.com/gkampitakis/go-snaps/snaps"
)
func TestAPIResponse(t *testing.T) {
resp := callAPI("/api/orders")
// Use testify for status code — simple assertion
assert.Equal(t, 200, resp.StatusCode)
// Use snapshot for body — complex output
snaps.MatchSnapshot(t, resp.Body)
}go-snaps Custom Matchers
go-snaps supports scrubbing non-deterministic values:
func TestOrderWithTimestamp(t *testing.T) {
order := CreateOrder()
// Scrub timestamp before snapshot
snaps.MatchSnapshot(t, scrubOrder(order))
}
func scrubOrder(o Order) Order {
o.ID = "<ID>"
o.CreatedAt = time.Time{} // Zero value
return o
}Handling Non-Deterministic Data
Both libraries require deterministic input. Common sources of non-determinism in Go:
Map Iteration Order
Go maps have undefined iteration order. Don't snapshot maps directly:
// BAD: map order is non-deterministic
data := map[string]int{"a": 1, "b": 2, "c": 3}
snaps.MatchSnapshot(t, fmt.Sprint(data)) // May produce different output each run
// GOOD: use a struct or sort the keys
type DataResult struct {
A int `json:"a"`
B int `json:"b"`
C int `json:"c"`
}
result := DataResult{A: 1, B: 2, C: 3}
b, _ := json.MarshalIndent(result, "", " ")
snaps.MatchSnapshot(t, string(b))Timestamps
// Use a fixed time in tests
func TestWithFixedTime(t *testing.T) {
fixedTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
order := createOrderAt(fixedTime)
b, _ := json.MarshalIndent(order, "", " ")
snaps.MatchSnapshot(t, string(b))
}Pointer Addresses
// BAD: printing a struct with %v includes pointer addresses
fmt.Sprintf("%v", &MyStruct{}) // &{0xc0001234}
// GOOD: serialize to JSON or use a stable string representationCupaloy vs. go-snaps Side by Side
| Feature | cupaloy | go-snaps |
|---|---|---|
| Update flag | UPDATE_SNAPSHOTS=true |
UPDATE_SNAPS=true |
| File format | Custom (configurable) | .snap format |
| Snapshot location | .snapshots/ (configurable) |
__snapshots__/ |
| First run behavior | Fails — output printed | Creates snapshot, passes |
| Testify integration | None | snaps.MatchSnapshot works with testify |
| Custom serializers | Via string conversion | Via MatchJSON, custom marshalers |
| Stars (GitHub) | ~900 | ~600 |
Choose cupaloy if you want simplicity and don't need testify integration. Choose go-snaps if you use testify and want richer snapshot management.
CI Configuration
In CI, never pass the update flag. If snapshots don't match, the test fails:
# .github/workflows/test.yml
- name: Run tests with snapshots
run: go test ./... -v
# No UPDATE_SNAPS or UPDATE_SNAPSHOTS env var
# Snapshot mismatches fail the buildMake sure snapshot files are committed:
git add **/__snapshots__/*.snap
git add **/.snapshots/*.txtAdd to .gitignore (if you have a received/temp pattern):
# cupaloy received files (if any)
*.received.txtWhen to Use Snapshot Tests in Go
| Good fit | Poor fit |
|---|---|
| Complex struct serialization | Simple values (use assert.Equal) |
| Template/report output | Random or time-dependent output |
| Code generator output | Very large outputs (hard to review) |
| HTTP response bodies | Tests that need to express why output is correct |
| CLI output formatting | Outputs with uncontrollable non-determinism |
Summary
Go has two good snapshot testing libraries:
- cupaloy — minimal API,
SnapshotT(t, value), good for simple cases - go-snaps — richer API, testify integration,
.snapformat with labeled entries
Both require:
- Deterministic input (no maps, timestamps, or random values)
- Snapshot files committed to source control
UPDATE_SNAPSHOTS/UPDATE_SNAPSenv var (not a flag) to regenerate
Use snapshot tests for complex output where hand-written assertions would span 20+ lines. Keep using assert.Equal for simple values — snapshot testing is a tool for specific cases, not a replacement for all assertions.