必须用bytes.buffer+multipart.writer构造请求体,确保boundary正确、字段名匹配、content-type头与writer.formdatacontenttype()一致,否则c.formfile()返回nil。

怎么用 multipart/form-data 构造测试请求
Gin 的文件上传接口依赖 *http.Request 中的 multipart.Form,测试时不能直接传 JSON 或字符串。必须手动构造符合 multipart/form-data 规范的请求体,否则 c.FormFile("file") 会返回 nil 或 ErrNoMultipartForm。
实操建议:
- 用
bytes.Buffer+multipart.Writer构建请求体,确保 boundary 正确、字段名与接口约定一致(比如字段名是file还是avatar) - 设置请求头
Content-Type: multipart/form-data; boundary=xxx,值必须和multipart.Writer生成的一致 - 避免手写 raw body —— 容易漏掉换行、空行或 boundary 结束符,导致解析失败
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
file, _ := writer.CreateFormFile("file", "test.png")
file.Write([]byte("fake-image-content"))
writer.Close()
req, _ := http.NewRequest("POST", "/upload", buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
为什么 c.FormFile() 在测试中总是返回 nil
这不是 Gin 的 bug,而是测试请求没触发 multipart 解析流程。Gin 只在首次调用 c.MultipartForm()、c.FormFile() 或 c.PostForm() 时才解析 body;如果测试里没走这个路径(比如提前 panic、中间件拦截、或用了 c.GetRawData()),form 就不会被初始化。
常见错误现象:
- 忘记调用
writer.Close()→ 请求体不完整 →ParseMultipartForm失败 →FormFile返回nil - 请求 method 不是
POST或PUT→ Gin 跳过 multipart 解析 - 中间件(如日志、鉴权)读取了
c.Request.Body但没重放 → body 流已关闭,后续FormFile无法读取
验证方式:在 handler 开头加 fmt.Printf("form: %+v\n", c.MultipartForm()),看是否为 nil。
如何测试文件大小超限、空文件、非法扩展名等边界情况
Gin 本身不校验文件名或大小,这些逻辑通常写在 handler 内。测试的关键是让 c.FormFile() 成功返回,再让业务逻辑执行判断分支。
实操建议:
- 空文件:用
writer.CreateFormFile("file", "")创建无内容的字段,FormFile仍会返回非 nil 的*multipart.FileHeader,但Size == 0 - 超大文件:用
bytes.Repeat([]byte("x"), 10*1024*1024)构造 10MB 内容,配合c.Request.ParseMultipartForm(5 (5MB 限制)触发 <code>http.ErrMissingBoundary或http.ErrNotMultipart - 非法扩展名:构造
filename="evil.exe",检查你的后缀白名单逻辑是否生效,而不是依赖 Gin 拦截
注意:c.MaxMultipartMemory 是 Gin 的内存限制(默认 32MB),它影响的是 ParseMultipartForm 能否成功,不是业务层的“文件大小校验” —— 后者应基于 FileHeader.Size 判断。
测试中要不要启动真实 HTTP server
不要。Gin 提供了 gin.CreateTestContext() 和 httptest.NewRecorder(),可完全绕过网络栈,直接把请求注入路由,性能高且可控。
实操要点:
- 用
gin.New()或gin.TestMode()初始化引擎,避免日志干扰断言 - 注册 handler 后,用
engine.ServeHTTP(recorder, req)执行,而非http.ListenAndServe - 检查
recorder.Code(状态码)、recorder.Body.String()(响应内容)、recorder.Header()(头信息)
真实 server 会引入端口占用、超时、并发等问题,对单元测试是过度设计。只有集成测试阶段才需要。
真正容易被忽略的是:multipart 表单中多个同名字段(如多文件上传)需要用 c.MultipartForm().File["files"] 获取切片,而 c.FormFile() 只取第一个 —— 测试时若没覆盖多文件场景,上线后可能出错。











