What is httptest, and how do you use it to test HTTP handlers?

5 minintermediategotestinghttptestnet-http

Quick Answer

net/http/httptest provides tools for testing HTTP code without a real network connection. httptest.NewRecorder() gives you an http.ResponseWriter implementation that captures whatever a handler writes (status code, headers, body) into memory, so you can call your handler directly and assert on the recorded response. httptest.NewServer(handler) spins up a real, local HTTP server on a random port backed by your handler, useful when testing code that needs an actual *http.Client making real HTTP requests (rather than calling the handler function directly). Both let you test HTTP-facing code fast and deterministically, without binding to a real network port for unit-level tests, or with a real (but local, ephemeral) server for integration-style tests.

Detailed Answer

httptest is the standard library's answer to "how do I test an HTTP handler without actually starting a server and making real network calls."

Testing a handler directly with NewRecorder

func helloHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "world"
    }
    fmt.Fprintf(w, "Hello, %s!", name)
}

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/hello?name=Alice", nil)
    rec := httptest.NewRecorder()

    helloHandler(rec, req)

    if rec.Code != http.StatusOK {
        t.Errorf("status = %d, want 200", rec.Code)
    }
    if body := rec.Body.String(); body != "Hello, Alice!" {
        t.Errorf("body = %q, want %q", body, "Hello, Alice!")
    }
}

No real network socket is opened at all — helloHandler is called as a plain function, and rec captures everything it writes for inspection.

Using a real local server with NewServer

func TestWithRealServer(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(helloHandler))
    defer server.Close()

    resp, err := http.Get(server.URL + "/hello?name=Bob")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if string(body) != "Hello, Bob!" {
        t.Errorf("got %q", body)
    }
}

httptest.NewServer starts a real TCP listener on an available local port, useful when testing a client library or code that genuinely needs to make an HTTP round trip, not just call a handler function in-process.

When to use which

Use NewRecorder for fast, in-process handler tests — the overwhelming majority of handler-level unit tests. Reach for NewServer when the code under test needs a real *http.Client making an actual request. Testing a client wrapper library, or verifying behavior specific to the net/http transport, are typical cases.

Related Resources