Go HTTP Handler 测试入门:不用启动端口也能测接口

本文讲解如何使用 httptest 测试 Go HTTP handler,包括请求构造、响应断言、JSON 校验和依赖替身。

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 设计成普通函数,这让测试天然简单,要把这个优势用起来。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页