We just open sourced expect, a small Go package for snapshot testing.
We built it because snapshots let us assert on the full output of something in a single line. Instead of writing a pile of assert.Equal() calls, we snapshot the whole thing and review the diff when it changes.
That makes tests easier to write, easier to maintain, and easier to review in code review.
Why we built it
We use snapshot tests when we want to verify the full output of something, not just one or two fields.
That is especially useful for:
- HTTP requests and responses
- JSON payloads
- SSE streams
- SQL generated by application code or ORMs
- structured logs
- mock call history
Snapshot testing works by saving output to a file on the first run, then comparing future runs against that saved version. If the output changes, the test fails.
That gives us a few things we want in back-end tests:
- broad coverage with very little test code
- one-line assertions over the whole output
- snapshots that are easy to review in diffs
- stronger guarantees around accidental behavior changes
- a forcing function to remove nondeterminism from tests
What expect does
expect is a thin layer on top of snapshot testing. It is not a new testing framework. It just makes common back-end assertions easy and consistent.
Here is the basic idea:
func TestUserResponse(t *testing.T) {
resp := httptest.NewRecorder()
resp.WriteHeader(http.StatusOK)
resp.Write([]byte(`{"id":"123","name":"Jane"}`))
expect.Response(t, resp.Result())
}
On first run, that creates a snapshot file. On later runs, it compares the current output against the committed snapshot.
A few helpers we use a lot
HTTP responses
For responses, we wanted a helper that snapshots the full HTTP response while removing the most common sources of noise.
expect.Response strips volatile headers like Date, Vary, CORS headers, and any header that ends with -request-id, then snapshots the result.
That means tests stay focused on the parts we actually care about.
func TestCreateUser(t *testing.T) {
recorder := httptest.NewRecorder()
recorder.Header().Set("Content-Type", "application/json")
recorder.WriteHeader(http.StatusCreated)
recorder.Write([]byte(`{"id":"user_123","name":"Jane Doe"}`))
expect.Response(t, recorder.Result())
}
SQL queries
SQL snapshots are one of the most useful parts of the package for us.
When SQL is built dynamically, especially through query builders or ORMs, it is easy for important changes to slip through. Snapshotting the final query gives us a simple way to review exactly what the database will see.
expect.SQL formats SQL with pgFormatter in Docker using our published image at ghcr.io/funnelstory/pgformatter, then snapshots it as a .sql file.
func TestUserQuery(t *testing.T) {
query := `
SELECT u.id, u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.active = true
ORDER BY o.created_at DESC
`
expect.SQL(t, query)
}
If Docker or the formatter is unavailable, expect.SQL snapshots the unformatted SQL instead. That makes first-time snapshot creation more forgiving, while still keeping the test output visible.
JSON, logs, streams, and more
The package also includes helpers for:
expect.Outputfor general snapshotsexpect.JSONfor indented JSONexpect.Requestfor HTTP requestsexpect.ResponseStreamfor SSE responses plus parsed eventsexpect.Logsfor Zap observer outputexpect.Callsfor mock call historyexpect.HTMLandexpect.OutputWithExtensionfor custom file types
The point is consistency. When tests follow the same pattern, snapshots are easier to trust and easier to review.
Snapshot testing only works if your system is deterministic
One nice side effect of snapshot tests is that they force you to clean up hidden nondeterminism.
If a snapshot changes every run, the test is telling you something useful.
In practice, that usually means controlling things like:
- time
- UUID generation
- random values
- map ordering
- floating point output
- request IDs and other per-request metadata
This has been one of the biggest benefits for us. Snapshot testing is a practical way to push a back-end toward deterministic behavior.
Why we decided to open source it
expect started as an internal utility. We kept using it across more of our codebase, so we pulled it out into a small reusable package.
There are already good snapshot tools in the Go ecosystem. What we wanted to share was a small set of back-end-focused helpers we kept reaching for: response cleanup, SQL snapshots, SSE helpers, log snapshots, and an API that stays close to normal testing.T usage.
If you are testing APIs, event streams, logs, or generated SQL in Go, this package might save you some boilerplate.
Try it out
You can install it with:
go get github.com/funnelstory/expect
The source is here:
If you try it and have ideas for improvements, we would love to hear them.