HTTP handler 測試的核心規則是不用啟動真實 server,也能驗證 request 進入 handler 後產生的 response。net/http/httptest 提供 request builder 與 response recorder,讓 handler 可以像普通函式一樣被測試。

httptest 把 HTTP 測試變成函式呼叫

httptest 的核心用途是建立測試用 request 與 response writer。handler 本來就是 func(http.ResponseWriter, *http.Request),所以測試可以直接呼叫 handler。

1func handleHealth(w http.ResponseWriter, r *http.Request) {
2    if r.Method != http.MethodGet {
3        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
4        return
5    }
6
7    w.Header().Set("Content-Type", "application/json")
8    fmt.Fprint(w, `{"status":"ok"}`)
9}

這個 handler 可以不用啟動 port,也不用發出真實網路請求。

 1func TestHandleHealth(t *testing.T) {
 2    req := httptest.NewRequest(http.MethodGet, "/health", nil)
 3    rec := httptest.NewRecorder()
 4
 5    handleHealth(rec, req)
 6
 7    res := rec.Result()
 8    defer res.Body.Close()
 9
10    if res.StatusCode != http.StatusOK {
11        t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK)
12    }
13}

httptest.NewRequest 建立 request,httptest.NewRecorder 記錄 response。測試直接呼叫 handleHealth(rec, req),再檢查 recorder 產生的結果。

status code 是第一個行為合約

HTTP response 的核心合約通常先看 status code。成功、輸入錯誤、方法不允許與伺服器錯誤,都應該有明確狀態碼。

 1func TestHandleHealthMethodNotAllowed(t *testing.T) {
 2    req := httptest.NewRequest(http.MethodPost, "/health", nil)
 3    rec := httptest.NewRecorder()
 4
 5    handleHealth(rec, req)
 6
 7    if rec.Code != http.StatusMethodNotAllowed {
 8        t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
 9    }
10}

rec.Code 可以直接取得 handler 寫出的狀態碼。若 handler 沒有呼叫 WriteHeader,但有寫 body,狀態碼通常會是 200

測試狀態碼時,不要只檢查 body 字串。body 可能改文案,但 status code 才是呼叫端最依賴的協定訊號。

body 檢查要符合輸出格式

response body 的核心檢查方式應該配合輸出格式。純文字可以比對字串;JSON 應該解析成 struct 或 map 後再比對欄位。

 1func TestHandleHealthBody(t *testing.T) {
 2    req := httptest.NewRequest(http.MethodGet, "/health", nil)
 3    rec := httptest.NewRecorder()
 4
 5    handleHealth(rec, req)
 6
 7    var body struct {
 8        Status string `json:"status"`
 9    }
10
11    if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
12        t.Fatalf("decode response body: %v", err)
13    }
14
15    if body.Status != "ok" {
16        t.Fatalf("status field = %q, want %q", body.Status, "ok")
17    }
18}

解析 JSON 後檢查欄位,比直接比對 {"status":"ok"} 更穩定。JSON 欄位順序、空白與換行不應該讓測試失敗。

request body 可以用 strings.NewReader

測試 JSON request 的核心做法是把字串或 bytes 包成 reader。handler 看到的是 io.Reader,不需要知道資料來自檔案、網路或測試字串。

 1func TestHandleCreateUser(t *testing.T) {
 2    body := strings.NewReader(`{"name":"Alice","email":"alice@example.com"}`)
 3    req := httptest.NewRequest(http.MethodPost, "/users", body)
 4    req.Header.Set("Content-Type", "application/json")
 5
 6    rec := httptest.NewRecorder()
 7    handler := newCreateUserHandler(fakeUserCreator{})
 8
 9    handler.ServeHTTP(rec, req)
10
11    if rec.Code != http.StatusCreated {
12        t.Fatalf("status = %d, want %d", rec.Code, http.StatusCreated)
13    }
14}

strings.NewReader 讓測試資料留在測試檔中,適合小型 JSON。若 request 很大或要重複使用,可以把測試資料放在 testdata 目錄。

依賴應該用 fake 隔離

handler 測試的核心邊界是 HTTP 行為,不是資料庫或外部服務。若 handler 需要呼叫內部服務,可以提供 fake 實作,讓測試專注於 request/response。

 1type fakeUserCreator struct {
 2    id  string
 3    err error
 4}
 5
 6func (f fakeUserCreator) CreateUser(ctx context.Context, name string, email string) (string, error) {
 7    if f.err != nil {
 8        return "", f.err
 9    }
10    return f.id, nil
11}

成功案例可以讓 fake 回傳 id,失敗案例可以讓 fake 回傳錯誤。這樣測試可以分別驗證 201 Created500 Internal Server Error

 1func TestHandleCreateUserServiceError(t *testing.T) {
 2    req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"name":"Alice","email":"alice@example.com"}`))
 3    rec := httptest.NewRecorder()
 4
 5    handler := newCreateUserHandler(fakeUserCreator{err: errors.New("database unavailable")})
 6    handler.ServeHTTP(rec, req)
 7
 8    if rec.Code != http.StatusInternalServerError {
 9        t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
10    }
11}

這是 handler 單元測試。資料庫連線、migration、真實網路等行為應該放在更高層級的整合測試處理。

下一章

下一章會處理時間注入,說明如何避免測試依賴真實現在時間。