Go测试需严格匹配规范:文件名以_test.go结尾,函数名以Test开头且参数为*testing.T;HTTP测试须用httptest.NewRecorder和router.ServeHTTP模拟完整生命周期,multipart请求体必须用bytes.Buffer+multipart.Writer构造并调用Close()。

测试文件命名和函数签名必须严格匹配 Go 规范
Go 的 testing 包只认两种东西:以 _test.go 结尾的文件,以及以 Test 开头、参数为 *testing.T 的函数。写成 testPing 或 ping_test(缺下划线)或 func TestPing(t int) 都不会被 go test 扫描到。
常见错误现象:
- 运行
go test显示no test files—— 文件名没带_test.go - 运行
go test -v没有输出任何=== RUN行 —— 函数名没以Test开头,或参数类型写错
实操建议:
- 直接用
go test -run ^Test.*$精确匹配函数名,避免拼写干扰 - 在 handler 所在包里新建
xxx_handler_test.go,和xxx_handler.go放同一目录 - 函数名尽量反映被测行为,比如
TestUploadHandler_WithValidFile比TestUpload1更易维护
构造请求必须用 httptest.NewRecorder() + router.ServeHTTP()
别用 http.Get 走真实网络——它绕过 Gin 的中间件链、不触发路由匹配、无法注入上下文值(如 c.Set("user_id", 123)),而且慢。
正确路径是模拟整个 HTTP 生命周期:构造请求 → 注入到 Gin 路由器 → 记录响应 → 断言结果。
实操建议:
- 用
http.NewRequest("POST", "/upload", nil)创建空请求,再用req.MultipartReader()或bytes.Buffer + multipart.Writer填充 body(见下一条) - 务必调用
router.ServeHTTP(w, req),不是handler(c)直接调用——后者跳过 Gin 的 context 初始化和中间件执行 - 响应记录器必须是
*httptest.ResponseRecorder,不能是自定义 struct,否则w.Body.String()会 panic
c.FormFile("file") 在测试中返回 nil 的真实原因
这不是 Gin 有问题,而是 multipart 解析根本没触发。Gin 只在首次调用 c.FormFile()、c.MultipartForm() 或 c.PostForm() 时才解析 body;如果测试里提前读了 c.Request.Body(比如中间件或日志),body 流就关了,后续 FormFile 必然失败。
实操建议:
- 构造请求体必须用
bytes.Buffer+multipart.Writer,且一定要调用writer.Close(),否则 boundary 结束符缺失,解析失败 - 字段名(如
"file")必须和 handler 里c.FormFile("file")的字符串完全一致,大小写、下划线都不能错 - 请求头
Content-Type必须设为writer.FormDataContentType()返回的值,手写"multipart/form-data"不带 boundary 会失效 - 在 handler 开头加
fmt.Printf("form: %+v\n", c.MultipartForm()),如果输出form: &{nil nil nil}就说明解析压根没走
边界情况测试要靠构造不同形态的 multipart 字段
Gin 不校验文件大小、扩展名或是否为空,这些逻辑都在 handler 里。测试的关键是让 c.FormFile() 成功返回一个非 nil 的 *multipart.FileHeader,再让业务代码走到 if 分支里去。
实操建议:
- 空文件:用
writer.CreateFormFile("file", "")创建无内容字段,fh.Size为 0,但c.FormFile()仍返回非 nil - 超大文件:手动设置
fh.Size = 1024 * 1024 * 51(51MB),然后在 handler 中检查是否触发了maxMemory限制逻辑 - 非法扩展名:传
"evil.php"作为第二参数给CreateFormFile,再验证 handler 是否拒绝 - 多个同名字段:调用两次
CreateFormFile("file", ...),看 handler 是否按预期只取第一个,或报错
最常被忽略的是:所有这些测试都依赖同一个 multipart.Writer 实例,一旦漏掉 writer.Close(),整个请求体格式就错,所有边界分支都跑不到——不是逻辑问题,是构造问题。











