testify, httptest & Test Doubles
Assertions, HTTP handler tests, and interface mocks.
Summary
The standard library covers running tests and recording HTTP responses.
testify adds readable assertion failures; httptest is stdlib for handlers; test doubles in Go are usually hand-written fakes behind small interfaces.
Recipe
Quick-reference recipe card - copy-paste ready.
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
HealthHandler(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `{"status":"ok"}`, rec.Body.String())
}When to reach for this:
- HTTP handlers need fast tests without
ListenAndServe - Assertion failures should show diffs for structs and JSON
- Production code already depends on interfaces you can fake
- You want
requireto stop setup after a failed precondition - gRPC or DB boundaries need injectable dependencies
Working Example
package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var ErrNotFound = errors.New("not found")
type UserStore interface {
Get(ctx context.Context, id string) (string, error)
}
type fakeStore struct {
users map[string]string
}
func (f *fakeStore) Get(ctx context.Context, id string) (string, error) {
if name, ok := f.users[id]; ok {
return name, nil
}
return "", ErrNotFound
}
func UserHandler(store UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
name, err := store.Get(r.Context(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
_ = json.NewEncoder(w).Encode(map[string]string{"name": name})
}
}
func TestUserHandler(t *testing.T) {
store := &fakeStore{users: map[string]string{"1": "Ada"}}
srv := httptest.NewServer(UserHandler(store))
t.Cleanup(srv.Close)
resp, err := http.Get(srv.URL + "?id=1")
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]string
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
assert.Equal(t, "Ada", body["name"])
}What this demonstrates:
fakeStoreimplementsUserStorewithout a mock generatorhttptest.NewServerexercises realhttp.Clientbehaviort.Cleanupcloses the server after the test- testify separates fatal preconditions (
require) from extra checks (assert)
Deep Dive
How It Works
httptest.NewRecorderimplementshttp.ResponseWriterin memory.httptest.NewServerbinds127.0.0.1:0and returns a client-ready URL.- testify compares values with
reflect.DeepEqualand formats diffs. - Fakes implement the same interface as production adapters (SQL, gRPC, S3).
httptest vs net/http Client
| Tool | Starts listener | Best for |
|---|---|---|
NewRecorder | No | Unit testing handler funcs |
NewServer | Yes (loopback) | Middleware chains, TLS, client timeouts |
httptest.NewRequest | No | Building *http.Request with context |
Go Notes
// gomock (optional): go.uber.org/mock/mockgen
// Generate: mockgen -destination=mocks/store_mock.go . UserStoreHand-written fakes stay readable for one or two methods; generators help wide interfaces.
Gotchas
- Forgetting to close Response.Body - leaks connections under
-count=100. Fix:defer resp.Body.Close()after every client call. - Asserting map iteration order in JSON - flaky string compare. Fix: Use
require.JSONEqor decode into structs. - Mocking concrete structs - Go mocks target interfaces. Fix: Extract a narrow interface at the call site.
- Over-mocking internal helpers - tests break on refactors without behavior change. Fix: Mock IO boundaries only.
- Global testify suite state -
suite.Suiteshares fields across tests. Fix: Prefer plainTestXxxwith tables unless hooks are essential. - Not setting request context - handlers that honor cancellation fail in tests. Fix:
req = req.WithContext(ctx)in setup.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Plain if got != want | Zero deps, tiny packages | Large struct comparisons |
cmp.Diff (google/go-cmp) | Precise struct diffs without testify | Team already standardized on testify |
go.uber.org/mock | Wide interfaces, many implementations | One-method fakes |
| Real docker DB (testcontainers) | SQL integration truth | Unit-testing handler JSON mapping |
FAQs
Is testify required for Go projects?
No.
Many codebases use only stdlib checks; testify is a convenience module.
require vs assert?
require calls t.FailNow() - use for setup preconditions.
assert records failure and continues - use for multiple independent expectations.
How do I test middleware?
Wrap a stub handler with your middleware, pass httptest.NewRecorder, and assert the recorder.
For full chain tests, use NewServer.
Can I use httptest with chi, gin, or echo?
Yes - mount routes on a http.Handler or call framework ServeHTTP with a recorder.
Framework context objects may need their test helpers.
Where should fake types live?
Keep small fakes in _test.go.
Move shared fakes to export_test.go or an internal/testutil package if many packages reuse them.
How do I simulate errors from dependencies?
Add fields on the fake struct (failNext bool, err error) toggled per table row.
Does testify work with t.Parallel?
Yes - each subtest gets its own *testing.T.
Avoid sharing mutable fakes across parallel subtests without synchronization.
How do I test gRPC without a real server?
Use bufconn with an in-memory listener or generated mocks for the client stub.
See grpc-protobuf section for patterns.
What about snapshot testing?
Golden files under testdata/ are the idiomatic Go snapshot.
testify snapshots are less common than JSONEq plus fixtures.
Should I mock http.RoundTripper?
Yes for client code that calls external APIs - implement RoundTrip on a fake.
Keeps tests offline and deterministic.
How do I assert HTTP headers?
rec.Header().Get("Content-Type") on a recorder, or resp.Header on client responses.
Can testify check error chains?
Use assert.ErrorIs and assert.ErrorAs - they wrap errors.Is / errors.As.
Related
- Go Testing Basics - httptest and testify intro
- Table-Driven Tests & Subtests - parametrized handler cases
- Examples as Executable Documentation - public API examples
- Coverage, Race Detector & Golden Files - snapshots and CI
- Testing Best Practices - pyramid and double policy
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).