httptest and API Testing Patterns

Updated

September 8, 2026

httptest and API Testing Patterns

Overview

net/http/httptest tests handlers without a real network. Combine with table tests and fake dependencies for fast API coverage.

Handler unit test

func TestHealth(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
    rec := httptest.NewRecorder()
    healthz(rec, req)
    if rec.Code != 200 {
        t.Fatalf("code %d", rec.Code)
    }
}

Full mux

srv := NewServer(fakeStore)
req := httptest.NewRequest(http.MethodGet, "/api/books", nil)
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, req)

JSON helpers

func decodeJSON[T any](t *testing.T, rec *httptest.ResponseRecorder) T {
    t.Helper()
    var v T
    if err := json.NewDecoder(rec.Body).Decode(&v); err != nil {
        t.Fatal(err)
    }
    return v
}

Auth cookies

req.AddCookie(&http.Cookie{Name: "session_id", Value: sid})

httptest.Server (integration-ish)

// Go 1.27+: in-memory network, t.Cleanup, synctest-safe.
ts := httptest.NewTestServer(t, handler)
res, err := ts.Client().Get("http://example.com/api/books")

Prefer NewTestServer for new tests. httptest.NewServer still listens on loopback when you need a real TCP URL:

ts := httptest.NewServer(handler)
t.Cleanup(ts.Close)
res, err := http.Get(ts.URL + "/api/books")

Still in-process; good middle ground before Testcontainers.

Rules of thumb

Do Don’t
Fake stores at interfaces Real network in unit tests
Table-drive status codes One mega test for all routes
t.Helper on asserts Ignore body on 500s

Try next

  1. Table test 200/400/401 for create book.
  2. Cookie session test.
  3. Assert Content-Type on JSON APIs.