Files
hotime/.cursor/plans/api接口测试方案_05b9404e.plan.md
T
hoteas de17ecbfd5 chore(logging): 更新日志重定向与捕获功能
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交
- 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获
- 更新 README 文档,增加对日志重定向功能的说明
2026-07-13 07:45:51 +08:00

300 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: API接口测试方案
overview: hotime 框架新增链式测试 API:数据在前方法在后,Post/Get 一步完成用例+断言,TestProj 合并注册,自动生成 Swagger UI。
todos:
- id: framework-testing-helper
content: hotimev1.5/testing_helper.goTestApp、NewTestApp、SetupForTest、TestRequest/WithSession、TestResponse
status: pending
- id: framework-testing-api
content: hotimev1.5/testing_api.goTestProj/ProjTest/CtrTest、Api(JSON/Query/Form/File/WithSession→Post/Get)、RunTests
status: pending
- id: framework-testing-swagger
content: hotimev1.5/testing_swagger.goGenerateSwagger 合并所有项目生成一份 Swagger
status: pending
- id: xbc-user-test
content: xbc/app/user.go 末尾添加 UserTestinit.go 添加 ProjectTest,新增 app_test.go
status: pending
isProject: false
---
# API 接口测试 + Swagger 文档自动生成(最终版)
## 一、最终链式设计:数据在前,方法在后
每条链以 `Post/Get/Put/Delete("描述", 期望status, [期望msg])` 结尾,这个终端调用同时完成:命名用例 + 发送请求 + 校验结果 + 收集文档。
```go
// 数据 → 方法(终端操作)
a.JSON(Map{"name": "138", "password": "123"}).Post("正常登录", 0)
a.JSON(Map{"name": "138", "password": ""}).Post("密码为空", 4, "用户名或密码不能为空")
a.Get("未登录", 2, "请先登录")
a.Query(Map{"shop_id": "1"}).Get("查询列表", 0)
a.WithSession(Map{"user_id": int64(1)}).Get("已登录", 0)
a.Query(Map{"token": "abc"}).JSON(Map{"phone": "138"}).Post("混合传参", 3, "异常")
a.File("file", "a.png", imgBytes).Post("上传文件", 0)
```
### 对比之前的写法
```
之前(三步):a.PostCase("正常登录").JSON(Map{...}).Test(0)
之前(两步):a.Post("正常登录", Map{...}).Test(0)
现在(一步):a.JSON(Map{...}).Post("正常登录", 0) ← 终端操作只有一个
现在(零数据):a.Get("未登录", 2, "请先登录") ← 最简一行
```
## 二、完整场景对照
```go
// POST JSON(最常见)
a.JSON(Map{"name": "138", "password": "123"}).Post("正常登录", 0)
// GET 无参数
a.Get("未登录", 2, "请先登录")
// GET + URL 参数
a.Query(Map{"shop_id": "1", "page": "1"}).Get("查询列表", 0)
// 需登录 + GET
a.WithSession(Map{"user_id": int64(1)}).Get("已登录", 0)
// 需登录 + GET + 参数
a.WithSession(Map{"user_id": int64(1)}).Query(Map{"shop_id": "1"}).Get("查询", 0)
// 需登录 + POST JSON
a.WithSession(Map{"user_id": int64(1)}).JSON(Map{"name": "new"}).Post("更新", 0)
// 混合:URL 参数 + JSON body
a.Query(Map{"token": "abc"}).JSON(Map{"phone": "138"}).Post("混合传参", 3, "异常")
// POST Form 表单
a.Form(Map{"shop_id": "1", "id": "1"}).Post("表单提交", 0)
// 文件上传
a.File("file", "a.png", imgBytes).Post("上传文件", 0)
// 文件 + 表单字段
a.File("file", "a.png", imgBytes).Form(Map{"type": "avatar"}).Post("上传+表单", 0)
// PUT JSON
a.JSON(Map{"name": "新名字"}).Put("更新信息", 0)
// DELETE + 参数
a.Query(Map{"id": "1"}).Delete("删除", 0)
// 校验响应数据(status=0 时)
resp := a.JSON(Map{"name": "138", "password": "123"}).Post("正常登录", 0)
if resp.GetBody().GetMap("result").GetString("token") == "" {
resp.Fail("缺少 token")
}
```
## 三、完整示例:user.go
```go
var UserCtr = Ctr{
"login": func(that *Context) { /* ... */ },
"info": func(that *Context) { /* ... */ },
"token": func(that *Context) { /* ... */ },
"create": func(that *Context) { /* ... */ },
"file": func(that *Context) { /* ... */ },
}
// ========= 接口测试 =========
var UserTest = CtrTest{
"login": {"用户账号密码登录", func(a *Api) {
a.JSON(Map{"name": "13800138000", "password": "123456"}).Post("正常登录", 0)
a.JSON(Map{"name": "13800138000", "password": ""}).Post("密码为空", 4, "用户名或密码不能为空")
a.JSON(Map{"name": "1380013", "password": "123456"}).Post("手机号格式错误", 4, "手机号码格式错误")
a.JSON(Map{"name": "13800138000", "password": "wrong"}).Post("密码错误", 4, "用户名或密码错误")
}},
"info": {"获取用户信息", func(a *Api) {
a.Get("未登录", 2, "请先登录")
a.WithSession(Map{"user_id": int64(1)}).Get("已登录", 0)
}},
"token": {"获取会话Token", func(a *Api) {
a.JSON(Map{"phone": "13800138000"}).Post("正确手机号", 0)
a.JSON(Map{"phone": "138"}).Post("手机号格式错误", 3, "请输入正确的手机号")
}},
"create": {"用户注册", func(a *Api) {
phone := "138" + ObjToStr(RandX(10000000, 99999999))
defer a.DB().Delete("user", Map{"phone": phone})
a.JSON(Map{"phone": phone, "password": "123456"}).Post("正常注册", 0)
a.JSON(Map{"phone": phone, "password": "123456"}).Post("重复注册", 3, "该手机号已被注册")
a.JSON(Map{"phone": "138", "password": "123456"}).Post("手机号格式错误", 3, "请输入正确的手机号")
a.JSON(Map{"phone": "13899999999", "password": "12"}).Post("密码太短", 3, "密码不能少于6位")
}},
"forget": {"忘记密码", func(a *Api) {
a.JSON(Map{"phone": "13800138000"}).Post("无token", 3, "异常")
a.Query(Map{"token": "abc"}).JSON(Map{"phone": "138", "code": "1234"}).Post("手机号错误", 3, "请输入正确的手机号")
}},
"file": {"文件上传", func(a *Api) {
a.File("file", "test.txt", []byte("hello")).Post("未登录上传", 2, "你还没有登录")
a.WithSession(Map{"user_id": int64(1)}).File("file", "test.txt", []byte("hello")).Post("已登录上传", 0)
}},
}
```
## 四、TestProj 注册 + app_test.go
### init.go
```go
var Project = Proj{
"user": UserCtr,
"goods": GoodsCtr,
// ...
}
var ProjectTest = ProjTest{
"user": UserTest,
"goods": GoodsTest,
}
```
### app_test.go
```go
package app
import (
"os"
"testing"
. "code.hoteas.com/golang/hotime"
)
var testApp *TestApp
func TestMain(m *testing.M) {
testApp = NewTestApp("../config/config.json", TestProj{
"app": {Project, ProjectTest},
})
code := m.Run()
testApp.GenerateSwagger("HoTime API", "2.1.0", testApp.Config.GetString("tpt"))
os.Exit(code)
}
func TestApi(t *testing.T) {
testApp.RunTests(t)
}
```
## 五、Swagger:多项目合并为一份文档
`GenerateSwagger` 遍历 `TestProj` 中所有项目的所有测试数据,生成**一份合并的 OpenAPI JSON**。每个项目作为 Swagger 的 tag 分组:
```json
{
"tags": [
{"name": "app/user", "description": "用户接口"},
{"name": "app/goods", "description": "货品接口"},
{"name": "customer/customer", "description": "客户端接口"}
],
"paths": {
"/app/user/login": { ... },
"/app/user/info": { ... },
"/customer/customer/bindPhone": { ... }
}
}
```
多项目不会覆盖,全部汇聚在同一个文件中,按 tag 分组展示。
## 六、指定运行范围
Go 的 `-run` 参数支持按子测试层级过滤(`/` 分隔正则匹配):
```bash
# 全部
go test ./app/... -v
# 只跑 user 模块的所有接口
go test ./app/... -v -run TestApi/user
# 只跑 user 模块的 login 接口
go test ./app/... -v -run TestApi/user/login
# 只跑 login 接口的"密码为空"用例
go test ./app/... -v -run TestApi/user/login/密码为空
# 跑 user 和 goods 两个模块
go test ./app/... -v -run "TestApi/(user|goods)"
# 只跑所有模块中包含"未登录"的用例
go test ./app/... -v -run "TestApi/.*/.*未登录"
```
**不需要框架做任何额外处理**Go 的 `t.Run()` 子测试机制天然支持。
## 七、框架类型设计
```go
// === 注册类型 ===
type TestProj map[string]TestProjDef
type TestProjDef struct { Proj Proj; Tests ProjTest }
type ProjTest map[string]CtrTest
type CtrTest map[string]ApiTestDef
type ApiTestDef struct { Desc string; Func func(a *Api) }
// === Api:起点,数据设置 ===
type Api struct {
app *TestApp
path string
t interface{}
desc string
session Map
}
// 数据设置(返回 *ApiCase,开始构建)
func (a *Api) JSON(body interface{}) *ApiCase
func (a *Api) Query(params Map) *ApiCase
func (a *Api) Form(body Map) *ApiCase
func (a *Api) File(field, name string, content []byte) *ApiCase
// Session(返回新 *Api,后续调用都带此 Session
func (a *Api) WithSession(s Map) *Api
// 直接终端(无数据时直接调用)
func (a *Api) Get(desc string, status int, msg ...string) *ApiResponse
func (a *Api) Post(desc string, status int, msg ...string) *ApiResponse
func (a *Api) Put(desc string, status int, msg ...string) *ApiResponse
func (a *Api) Delete(desc string, status int, msg ...string) *ApiResponse
// 数据库
func (a *Api) DB() *HoTimeDB
// === ApiCase:链式构建器 ===
type ApiCase struct { /* api, session, query, jsonBody, formBody, file... */ }
// 继续叠加数据
func (c *ApiCase) JSON(body interface{}) *ApiCase
func (c *ApiCase) Query(params Map) *ApiCase
func (c *ApiCase) Form(body Map) *ApiCase
func (c *ApiCase) File(field, name string, content []byte) *ApiCase
func (c *ApiCase) WithSession(s Map) *ApiCase
// 终端操作(HTTP 方法 + 用例名 + 断言)
func (c *ApiCase) Get(desc string, status int, msg ...string) *ApiResponse
func (c *ApiCase) Post(desc string, status int, msg ...string) *ApiResponse
func (c *ApiCase) Put(desc string, status int, msg ...string) *ApiResponse
func (c *ApiCase) Delete(desc string, status int, msg ...string) *ApiResponse
// === ApiResponse ===
type ApiResponse struct { StatusCode int; Body Map; RawBody []byte }
func (r *ApiResponse) GetStatus() int
func (r *ApiResponse) GetResult() interface{}
func (r *ApiResponse) GetMsg() string
func (r *ApiResponse) GetBody() Map
func (r *ApiResponse) Fail(msg string) // 自定义断言失败
```
## 八、文件清单
- **新增** `d:/work/hotimev1.5/testing_helper.go`
- **新增** `d:/work/hotimev1.5/testing_api.go`
- **新增** `d:/work/hotimev1.5/testing_swagger.go`
- **修改** `d:/work/your-app/app/user.go` -- 末尾添加 UserTest
- **修改** `d:/work/your-app/app/init.go` -- 添加 ProjectTest
- **新增** `d:/work/your-app/app/app_test.go`