Skip to content

Day 045 — httptest for Handlers

Month 2 · Week 3 · ⬅ Day 044 · Day 046 ➡ · Journal index

🎯 Learning Objective

Test http.Handler code two ways with net/http/httptest: in-process with NewRecorder, and over a real loopback connection with NewServer.

📚 Topics

  • httptest.NewRequest + httptest.NewRecorder (no socket)
  • httptest.NewServer / NewTLSServer (real ephemeral port)
  • Asserting status, headers, and body

📖 Reading / Sources

📝 Notes

  • An http.Handler is anything with ServeHTTP(w http.ResponseWriter, r *http.Request). Because that's an interface, you can call it directly in a test — no server required. → [[http-handler]]
  • httptest.NewRecorder() returns a *ResponseRecorder, an http.ResponseWriter that captures Code, Header(), and Body (a *bytes.Buffer). Call h.ServeHTTP(rec, req) then assert.
  • httptest.NewRequest(method, target, body) builds a request wired for server-side handling (it panics on a bad target, which is fine in tests).
  • rec.Result() gives an *http.Response snapshot; read res.Body with io.ReadAll and close it. Or inspect rec.Code / rec.Body.String() directly.
  • httptest.NewServer(h) starts a real server on 127.0.0.1:<random>; use srv.URL with a real http.Client. Always defer srv.Close(). Reach for it when testing a client, middleware tied to the transport, redirects, or TLS (NewTLSServer).
  • Prefer NewRecorder — it's faster, deterministic, and avoids ports/goroutines. Use NewServer only when you genuinely need the wire.
  • Set the context on a request with req.WithContext(ctx) when the handler reads deadlines/values; the context is always the request's, not a separate arg.

💻 Code Examples

func TestHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/greet?name=Ada", nil)
    rec := httptest.NewRecorder()

    Handler().ServeHTTP(rec, req) // call directly — no network

    res := rec.Result()
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        t.Fatalf("status = %d; want 200", res.StatusCode)
    }
    body, _ := io.ReadAll(res.Body)
    if string(body) != "Hello, Ada!" {
        t.Errorf("body = %q; want %q", body, "Hello, Ada!")
    }
}

Runnable demo (both recorder + server): examples/month-02/httptest-demo/ · Run: go run ./examples/month-02/httptest-demo Tested handler: exercises/month-02/week-3/greeter/

🏋️ Exercises / Practice

Exercise Status Link
greeter.Handler with httptest table tests exercises/month-02/week-3/greeter
Assert the Allow header on a 405 exercises/month-02/week-3/greeter

🐛 Mistakes Made

  • Forgot to Close() the body from rec.Result() — harmless for a recorder but a real leak against NewServer; made it a habit.
  • Reached for NewServer first; realized NewRecorder covered everything except header-on-the-wire cases and was much faster.

❓ Open Questions

  • For middleware that wraps http.RoundTripper, do I test with NewServer or a fake RoundTripper? (Both valid; a fake transport is more unit-y.)

🧠 Active Recall (answer without looking)

  1. Q: Why can you test a handler without starting a server?
    A

http.Handler is an interface; you can construct a request and a ResponseRecorder (an http.ResponseWriter) and call ServeHTTP directly in-process. 2. Q: When is httptest.NewServer actually needed?

A

When you need the real transport: testing an HTTP client, redirects, connection/TLS behavior, or middleware tied to the network layer.

🪶 Feynman Reflection

A handler is just a function that fills in a response object. httptest.NewRecorder hands it a fake response object you can read back, so you "call the website" without any networking. NewServer is the real thing on a throwaway port for when you must use an actual client.

🕳️ Knowledge Gaps

  • TLS test servers and cert handling (NewTLSServer) — note for later.

✅ Summary

I can unit-test handlers in-process with a recorder and integration-test over a real loopback server, asserting status, headers, and body.

⏭️ Next Steps / Prep for Tomorrow

  • Day 046: measuring coverage and a first look at the testify assertion library.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: test(week-3): httptest recorder and server (day 045)