httptest for Handler Integration Tests
The net/http/httptest package tests HTTP handlers without manual TCP setup.
Search across all documentation pages
The net/http/httptest package tests HTTP handlers without manual TCP setup.
ResponseRecorder supports fast unit-style handler tests; NewServer supports integration tests that exercise real http.Client round trips.
Handler tests should assert status codes, headers, and bodies against expected behavior.
httptest.NewRequest constructs *http.Request values with method, URL, and body.
httptest.ResponseRecorder implements http.ResponseWriter and records what the handler wrote.
httptest.NewServer wraps a handler with an ephemeral http.Server and returns a base URL for client calls.
Table-driven tests keep route matrices maintainable.
Quick-reference recipe card - copy-paste ready.
func TestHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/items", strings.NewReader(`{"name":"a"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
}When to reach for this:
package main
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type item struct {
Name string `json:"name"`
}
func itemsHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode([]item{{Name: "seed"}})
case http.MethodPost:
var in item
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(in)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func TestItemsHandler_recorder(t *testing.T) {
tests := []struct {
name string
method string
body string
want int
}{
{"get ok", http.MethodGet, "", http.StatusOK},
{"post ok", http.MethodPost, `{"name":"x"}`, http.StatusCreated},
{"post bad json", http.MethodPost, `{`, http.StatusBadRequest},
{"patch not allowed", http.MethodPatch, "", http.StatusMethodNotAllowed},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var body io.Reader
if tc.body != "" {
body = strings.NewReader(tc.body)
}
req := httptest.NewRequest(tc.method, "/items", body)
if tc.body != "" {
req.Header.Set("Content-Type", "application/json")
}
rec := httptest.NewRecorder()
itemsHandler(rec, req)
if rec.Code != tc.want {
t.Fatalf("got %d want %d body=%q", rec.Code, tc.want, rec.Body.String())
}
})
}
}
func TestItemsHandler_newServer(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(itemsHandler))
defer srv.Close()
resp, err := http.Get(srv.URL + "/items")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status=%d", resp.StatusCode)
}
}What this demonstrates:
ResponseRecorder for direct handler invocation.strings.NewReader on NewRequest.httptest.NewServer black-box test using http.Get against srv.URL.NewRecorder returns a ResponseRecorder with Code, Header, and Body fields populated after ServeHTTP.NewRequest sets URL.Path and Host suitable for handler code that reads r.URL and r.Host.NewServer picks a free port, starts listening in a goroutine, and blocks shutdown on Close().NewTLSServer provides TLS for testing cert validation paths.| Pattern | Tool | Best for |
|---|---|---|
| Handler unit test | ResponseRecorder | Status/body/header assertions |
| Middleware test | Recorder + stub next | Short-circuit behavior |
| Mux routing test | mux.ServeHTTP(rec, req) | Pattern matching |
| Client integration | NewServer + http.Client | Full round trip |
// Assert JSON without brittle string compares:
var got item
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
// Run server tests in parallel with their own srv:
t.Parallel()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)Close() on NewServer - Leaks goroutines and ports in repeated test runs. Fix: defer srv.Close() or t.Cleanup(srv.Close).t.Parallel. Fix: Create fresh http.NewServeMux() per test or use t.Parallel only with isolated handlers.json.RawMessage comparisons.http.DefaultClient without timeout in tests - Hanging tests if server stalls. Fix: Short http.Client{Timeout: ...} in integration tests.rec.Code defaults to 200 - Zero means unset until WriteHeader or first Write. Fix: Check Code after handler returns; default is 200 only after write.
| Alternative | Use When | Don't Use When |
|---|---|---|
| ginkgo/gomega HTTP matchers | BDD-style specs preferred | Standard testing package suffices |
| dockerized integration | Full stack with real DB | Handler logic only needs in-memory tests |
| recorded fixtures | Stable third-party API contracts | Testing your own handlers in-process |
manual net.Listen | Custom TLS or HTTP/2 setup | httptest.NewServer covers most cases |
Use ResponseRecorder for direct handler unit tests.
Use NewServer when the test must exercise http.Client, redirects, or TLS.
Set Authorization or session cookies on httptest.NewRequest before invoking the handler chain.
Yes - call mux.ServeHTTP(rec, req) with method and path matching registered patterns.
Read rec.Header().Get("Content-Type") after the handler returns.
rec.Result() returns an *http.Response snapshot for advanced cases.
It uses context.Background() by default.
Use req.WithContext(ctx) to test cancellation behavior.
Pass a custom ResponseWriter or use recorder and assert the wrapper observed the status.
NewUnstartedServer plus manual ConfigureServer can enable HTTP/2.
Most handler tests do not require HTTP/2-specific behavior.
Build multipart.Writer body, set Content-Type boundary header, and pass the buffer to NewRequest.
Yes when each subtest owns its own server or mux.
Avoid parallel tests mutating shared global http.DefaultServeMux.
Redirect log output to bytes.Buffer in tests or inject a logger interface into handlers.
Start a NewServer for the fake upstream and mount ReverseProxy as the handler under test.
Exercise recovery middleware with a handler that panics and assert 500 status on the recorder.
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 19, 2026