Go

How do you unit-test HTTP handlers and HTTP clients with net/http/httptest?

Question 473MediumGo 1.22 to 1.25

For handlers, use httptest.NewRecorder() and httptest.NewRequest — no network, fast, and you inspect the recorded status, headers and body. Route through your real mux so Go 1.22 path values are populated (or call req.SetPathValue). For clients, spin up httptest.NewServer (a real loopback server) and point the client's base URL at srv.URL.

func TestGetItem(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /items/{id}", getItem)

	tests := []struct {
		path string
		want int
	}{
		{"/items/42", http.StatusOK},
		{"/items/abc", http.StatusBadRequest},
	}
	for _, tc := range tests {
		t.Run(tc.path, func(t *testing.T) {
			t.Parallel()
			rec := httptest.NewRecorder()
			req := httptest.NewRequest(http.MethodGet, tc.path, nil)
			mux.ServeHTTP(rec, req)
			if rec.Code != tc.want {
				t.Fatalf("status = %d, want %d; body=%q", rec.Code, tc.want, rec.Body.String())
			}
		})
	}
}

func TestClientRetriesOn503(t *testing.T) {
	var calls atomic.Int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if calls.Add(1) == 1 {
			w.WriteHeader(http.StatusServiceUnavailable)
			return
		}
		io.WriteString(w, `{"ok":true}`)
	}))
	t.Cleanup(srv.Close)

	c := NewAPIClient(srv.URL, srv.Client())
	if err := c.Ping(context.Background()); err != nil {
		t.Fatal(err)
	}
	if got := calls.Load(); got != 2 {
		t.Fatalf("calls = %d, want 2", got)
	}
}

Design tip interviewers like: accept an *http.Client or a small interface (Do(*http.Request) (*http.Response, error)) via constructor injection so tests can substitute it; httptest.NewTLSServer tests TLS paths.

More on Standard Library, HTTP & Systems Design in Go

All 35 Standard Library, HTTP & Systems Design in Go questions