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

10 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
API接口测试方案 hotime 框架新增链式测试 API:数据在前方法在后,Post/Get 一步完成用例+断言,TestProj 合并注册,自动生成 Swagger UI。
id content status
framework-testing-helper hotimev1.5/testing_helper.goTestApp、NewTestApp、SetupForTest、TestRequest/WithSession、TestResponse pending
id content status
framework-testing-api hotimev1.5/testing_api.goTestProj/ProjTest/CtrTest、Api(JSON/Query/Form/File/WithSession→Post/Get)、RunTests pending
id content status
framework-testing-swagger hotimev1.5/testing_swagger.goGenerateSwagger 合并所有项目生成一份 Swagger pending
id content status
xbc-user-test xbc/app/user.go 末尾添加 UserTestinit.go 添加 ProjectTest,新增 app_test.go pending
false

API 接口测试 + Swagger 文档自动生成(最终版)

一、最终链式设计:数据在前,方法在后

每条链以 Post/Get/Put/Delete("描述", 期望status, [期望msg]) 结尾,这个终端调用同时完成:命名用例 + 发送请求 + 校验结果 + 收集文档。

// 数据 → 方法(终端操作)
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, "请先登录")                   ← 最简一行

二、完整场景对照

// 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

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

var Project = Proj{
    "user":  UserCtr,
    "goods": GoodsCtr,
    // ...
}

var ProjectTest = ProjTest{
    "user":  UserTest,
    "goods": GoodsTest,
}

app_test.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 分组:

{
  "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 参数支持按子测试层级过滤(/ 分隔正则匹配):

# 全部
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() 子测试机制天然支持。

七、框架类型设计

// === 注册类型 ===
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