← 中文资源

简体中文版本

测试用 HTTP 桩服务

在进程内启动桩服务器返回预设 JSON,原子统计请求次数,并验证未匹配路径的 404 行为。

阅读英文原文

功能说明

依赖外部接口的测试通常不稳定:网络抖动、限流、数据变化都会让用例失败。httptest 可以在当前进程内启动一个真实监听端口的服务器,客户端代码无需任何修改就能连上去,返回内容完全由测试决定。这种方式比接口 mock 更接近真实调用链,能覆盖序列化、状态码和连接处理。

桩服务以路径为键保存响应,未命中路径返回 404。请求计数使用原子操作,多个协程并发调用也不会丢计数,可在测试结束时断言调用次数。

关键代码

NewStubServer 返回 *httptest.Server,内部用 http.HandlerFunc 处理每个请求:先原子累加计数,再查表;命中时设置 JSON 内容类型、写入预设状态码与响应体,未命中交给 http.NotFound。main 依次请求用户、健康检查和不存在路径,打印状态码与响应体,最后读取总请求数。服务器通过 defer 关闭,释放监听端口。

使用注意

桩服务器会真实占用本地端口,测试并行时要避免共享同一实例。响应体中的 JSON 建议用结构体序列化生成,手写字符串容易出现转义错误。需要模拟超时或连接中断时,应使用自定义 RoundTripper 或直接关闭连接,而不是在处理器里长时间阻塞。测试结束务必关闭服务器,否则可能造成协程泄漏。真实项目中可把 *testing.T 传入并在 Cleanup 中关闭。

验证方式

三条请求分别返回 200 加用户 JSON、200 加健康检查 JSON、404 加默认未找到文本。总请求数应为三,说明计数覆盖了成功与失败路径。可以再加入并发请求,确认计数仍然准确,并观察每次响应都带有 application/json 内容类型。

代码示例

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
)

type StubResponse struct {
	Status int
	Body   string
}

func NewStubServer(responses map[string]StubResponse, hits *int64) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if hits != nil {
			atomic.AddInt64(hits, 1)
		}
		response, ok := responses[r.URL.Path]
		if !ok {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(response.Status)
		_, _ = io.WriteString(w, response.Body)
	}))
}

func main() {
	var hits int64
	server := NewStubServer(map[string]StubResponse{
		"/users/1": {Status: http.StatusOK, Body: `{"id":1,"name":"ada"}`},
		"/health":  {Status: http.StatusOK, Body: `{"status":"ok"}`},
	}, &hits)
	defer server.Close()

	for _, requestPath := range []string{"/users/1", "/health", "/missing"} {
		response, err := http.Get(server.URL + requestPath)
		if err != nil {
			panic(err)
		}
		body, err := io.ReadAll(response.Body)
		response.Body.Close()
		if err != nil {
			panic(err)
		}
		fmt.Printf("%s -> %d %s\n", requestPath, response.StatusCode, string(body))
	}
	fmt.Println("total requests:", atomic.LoadInt64(&hits))
}