Handler 不一定要启动服务才能测试
很多人测试 HTTP 接口时,会先 go run . 启动服务,再用 curl 或浏览器访问。这种手动测试有用,但不适合长期保护代码。每次改 handler 都手动点一遍,很快会漏。Go 标准库提供了 net/http/httptest,可以在测试里构造请求、记录响应,不需要真的监听端口。
HTTP handler 在 Go 里本质是一个函数:
func(w http.ResponseWriter, r *http.Request)
既然是函数,就可以直接调用。httptest.NewRequest 帮你创建请求,httptest.NewRecorder 帮你记录响应。这样你可以在单元测试里检查状态码、响应头和 JSON 内容。
这篇文章用一个文章创建接口做例子,讲清楚 handler 测试的基本套路。
最小 handler 测试
先写一个健康检查:
func healthHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
}
测试:
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
resp := rec.Result()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
if strings.TrimSpace(string(body)) != "ok" {
t.Fatalf("body = %q, want ok", string(body))
}
}
没有端口,没有真实网络,测试速度很快。rec.Result() 会把 recorder 转成 *http.Response,方便读取状态和 body。
测试方法不允许
继续测错误路径:
func TestHealthHandlerMethodNotAllowed(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/healthz", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
}
}
这里直接用 rec.Code 也可以。测试 handler 时,不要只测成功路径。HTTP 接口最容易出问题的地方往往是方法、参数、JSON 格式和依赖失败。
测试 JSON 响应
假设有响应函数:
type Article struct {
ID int64 `json:"id"`
Title string `json:"title"`
}
func writeJSON(w http.ResponseWriter, status int, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(value)
}
Handler:
func articleHandler(w http.ResponseWriter, r *http.Request) {
article := Article{ID: 1, Title: "Go 测试入门"}
writeJSON(w, http.StatusOK, article)
}
测试:
func TestArticleHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/article", nil)
rec := httptest.NewRecorder()
articleHandler(rec, req)
resp := rec.Result()
defer resp.Body.Close()
if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "application/json") {
t.Fatalf("content-type = %q", got)
}
var article Article
if err := json.NewDecoder(resp.Body).Decode(&article); err != nil {
t.Fatalf("decode response: %v", err)
}
if article.ID != 1 || article.Title != "Go 测试入门" {
t.Fatalf("article = %+v", article)
}
}
不要用字符串直接比较整段 JSON,除非顺序和格式完全固定。解码成结构体再断言字段,测试更稳。
用依赖替身测试 handler
真实 handler 往往依赖 service:
type ArticleService interface {
Create(ctx context.Context, title string) (Article, error)
}
type Handler struct {
service ArticleService
}
创建接口:
type createArticleRequest struct {
Title string `json:"title"`
}
func (h *Handler) createArticle(w http.ResponseWriter, r *http.Request) {
var req createArticleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
article, err := h.service.Create(r.Context(), req.Title)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusCreated, article)
}
测试替身:
type fakeArticleService struct {
title string
err error
}
func (s *fakeArticleService) Create(ctx context.Context, title string) (Article, error) {
s.title = title
if s.err != nil {
return Article{}, s.err
}
return Article{ID: 10, Title: title}, nil
}
测试:
func TestCreateArticle(t *testing.T) {
service := &fakeArticleService{}
h := &Handler{service: service}
body := strings.NewReader(`{"title":"Go Handler 测试"}`)
req := httptest.NewRequest(http.MethodPost, "/articles", body)
rec := httptest.NewRecorder()
h.createArticle(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusCreated)
}
if service.title != "Go Handler 测试" {
t.Fatalf("service title = %q", service.title)
}
}
这个测试没有数据库,也没有真实 service。它只验证 handler 是否正确解析请求,并调用依赖。
小结
httptest 让 Go HTTP handler 测试变得很轻。你可以用 httptest.NewRequest 构造请求,用 httptest.NewRecorder 捕获响应,检查状态码、响应头和 JSON 内容。测试不需要启动端口,也不需要真实网络。
Handler 测试最适合覆盖 HTTP 边界:方法不允许、请求体非法、参数缺失、依赖返回错误、成功响应格式。业务规则本身应该更多放在 service 测试里,handler 测试不要变成大型端到端测试。
当接口有自动化测试保护后,你改路由、响应格式和错误处理时会更有底。Go 把 handler 设计成普通函数,这让测试天然简单,要把这个优势用起来。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。