refactor(db): 增强测试事务支持和保存点管理
- 在 HoTimeDB 中新增测试事务功能,允许在测试模式下使用事务进行操作 - 实现 BeginTestTx 和 RollbackTestTx 方法,支持测试事务的开启和回滚 - 在 Action 方法中集成保存点管理,确保在测试模式下的嵌套事务处理 - 更新 README.md,添加 API 测试框架的相关说明,提升文档完整性
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
---
|
||||
name: API接口测试方案
|
||||
overview: hotime 框架新增链式测试 API:数据在前方法在后,Post/Get 一步完成用例+断言,TestProj 合并注册,自动生成 Swagger UI。
|
||||
todos:
|
||||
- id: framework-testing-helper
|
||||
content: hotimev1.5/testing_helper.go:TestApp、NewTestApp、SetupForTest、TestRequest/WithSession、TestResponse
|
||||
status: pending
|
||||
- id: framework-testing-api
|
||||
content: hotimev1.5/testing_api.go:TestProj/ProjTest/CtrTest、Api(JSON/Query/Form/File/WithSession→Post/Get)、RunTests
|
||||
status: pending
|
||||
- id: framework-testing-swagger
|
||||
content: hotimev1.5/testing_swagger.go:GenerateSwagger 合并所有项目生成一份 Swagger
|
||||
status: pending
|
||||
- id: xbc-user-test
|
||||
content: xbc/app/user.go 末尾添加 UserTest,init.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("小帮菜 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/xbc/app/user.go` -- 末尾添加 UserTest
|
||||
- **修改** `d:/work/xbc/app/init.go` -- 添加 ProjectTest
|
||||
- **新增** `d:/work/xbc/app/app_test.go`
|
||||
Reference in New Issue
Block a user