testify, httptest & Test Doubles
Assertions, HTTP handler tests, and interface mocks.
Search across all documentation pages
Assertions, HTTP handler tests, and interface mocks.
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.
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:
ListenAndServerequire to stop setup after a failed preconditionpackage 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:
fakeStore implements UserStore without a mock generatorhttptest.NewServer exercises real http.Client behaviort.Cleanup closes the server after the testrequire) from extra checks (assert)httptest.NewRecorder implements http.ResponseWriter in memory.httptest.NewServer binds 127.0.0.1:0 and returns a client-ready URL.reflect.DeepEqual and formats diffs.| 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 |
// 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.
-count=100. Fix: defer resp.Body.Close() after every client call.require.JSONEq or decode into structs.suite.Suite shares fields across tests. Fix: Prefer plain TestXxx with tables unless hooks are essential.req = req.WithContext(ctx) in setup.| 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 |
No.
Many codebases use only stdlib checks; testify is a convenience module.
require calls t.FailNow() - use for setup preconditions.
assert records failure and continues - use for multiple independent expectations.
Wrap a stub handler with your middleware, pass httptest.NewRecorder, and assert the recorder.
For full chain tests, use NewServer.
Yes - mount routes on a http.Handler or call framework ServeHTTP with a recorder.
Framework context objects may need their test helpers.
Keep small fakes in _test.go.
Move shared fakes to export_test.go or an internal/testutil package if many packages reuse them.
Add fields on the fake struct (failNext bool, err error) toggled per table row.
Yes - each subtest gets its own *testing.T.
Avoid sharing mutable fakes across parallel subtests without synchronization.
Use bufconn with an in-memory listener or generated mocks for the client stub.
See grpc-protobuf section for patterns.
Golden files under testdata/ are the idiomatic Go snapshot.
testify snapshots are less common than JSONEq plus fixtures.
Yes for client code that calls external APIs - implement RoundTrip on a fake.
Keeps tests offline and deterministic.
rec.Header().Get("Content-Type") on a recorder, or resp.Header on client responses.
Use assert.ErrorIs and assert.ErrorAs - they wrap errors.Is / errors.As.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026