chore(logging): 更新日志重定向与捕获功能
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交 - 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获 - 更新 README 文档,增加对日志重定向功能的说明
This commit is contained in:
+125
-215
@@ -2,10 +2,17 @@
|
||||
|
||||
不启动 HTTP 服务、自动事务回滚、链式 API、覆盖率报告、API 调试控制台生成。
|
||||
|
||||
> **测试执行铁律(日常开发 / AI 辅助必读)**
|
||||
>
|
||||
> - **只允许**用 `-run` 精确跑当前接口或当前控制器
|
||||
> - **禁止**把 `go test ./app/... -v`(不带 `-run`)当作默认命令
|
||||
> - 全量回归与覆盖率报告见文末「全量回归与覆盖率」,仅 CI / 发版前使用
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [快速开始](#快速开始)
|
||||
- [运行测试(推荐:-run 单测)](#运行测试推荐-run-单测)
|
||||
- [用例编写范式](#用例编写范式)
|
||||
- [错误用例编写规范](#1-错误用例编写规范)
|
||||
- [测试数据准备](#2-测试数据准备)
|
||||
@@ -14,14 +21,11 @@
|
||||
- [简写支持说明](#简写支持说明)
|
||||
- [核心类型](#核心类型)
|
||||
- [链式 API 参考](#链式-api-参考)
|
||||
- [API 速查表](#api-速查表)
|
||||
- [事务隔离机制](#事务隔离机制)
|
||||
- [覆盖率报告](#覆盖率报告)
|
||||
- [api-spec.json 覆盖率字段说明](#api-specjson-覆盖率字段说明)
|
||||
- [API 调试控制台](#api-调试控制台)
|
||||
- [指定运行范围](#指定运行范围)
|
||||
- [并发保护与缓存隔离](#并发保护与缓存隔离)
|
||||
- [二进制与非 JSON 响应校验](#二进制与非-json-响应校验)
|
||||
- [全量回归与覆盖率(仅 CI / 发版前)](#全量回归与覆盖率仅-ci--发版前)
|
||||
|
||||
---
|
||||
|
||||
@@ -185,6 +189,7 @@ func TestMain(m *testing.M) {
|
||||
"app": {Proj: Project, Tests: ProjectTest},
|
||||
})
|
||||
code := m.Run()
|
||||
// 以下两行适合 CI / 全量回归;日常用 -run 单测时也会执行,但覆盖率主要在全量时有意义
|
||||
testApp.PrintCoverage()
|
||||
testApp.GenerateSwagger("My API", "1.0.0", testApp.Config.GetString("tpt"))
|
||||
os.Exit(code)
|
||||
@@ -207,13 +212,30 @@ app/
|
||||
└── init_test.go ← 测试注册 (ProjectTest) + TestMain + TestApi
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
### 运行测试(推荐:`-run` 单测)
|
||||
|
||||
`RunTests` 使用 Go 标准 `t.Run()` 子测试,**日常开发请始终加 `-run`**:
|
||||
|
||||
```bash
|
||||
go test ./app/... -v
|
||||
# 只跑 order/create 接口(推荐,开发默认)
|
||||
go test ./app/ -v -run TestApi/app/order/create
|
||||
|
||||
# 只跑 order 控制器
|
||||
go test ./app/ -v -run TestApi/app/order
|
||||
|
||||
# 只跑某一条用例
|
||||
go test ./app/ -v -run "TestApi/app/order/create/正常创建订单"
|
||||
|
||||
# 同时跑 user 和 order
|
||||
go test ./app/ -v -run "TestApi/app/(user|order)"
|
||||
|
||||
# 跑所有控制器中包含「未登录」的用例
|
||||
go test ./app/ -v -run "TestApi/.*/.*未登录"
|
||||
```
|
||||
|
||||
> **注意**:`TestMain` 中的 `os.Chdir("..")` 确保 `go test ./app/` 的工作目录为项目根目录,使配置文件路径和模板输出路径正确。
|
||||
>
|
||||
> 全量 `go test ./app/... -v`(不带 `-run`)会跑完所有用例并触发覆盖率扫描,项目变大后很慢且容易牵连无关失败。**仅在 CI / 发版前使用**,说明见文末。
|
||||
|
||||
---
|
||||
|
||||
@@ -628,94 +650,29 @@ resp.ExpectResult(Map{"version": "sample", "features": Slice{}})
|
||||
| `resp.ExpectResult(sample)` | `*ApiResponse` | 后置结构校验(类型样本) |
|
||||
| `resp.Fail(msg)` | — | 手动标记测试失败 |
|
||||
|
||||
---
|
||||
|
||||
## API 速查表
|
||||
|
||||
以下为各种参数组合的快速参考。`a` 为 `*Api` 参数。
|
||||
|
||||
### 请求方式
|
||||
### 常用写法速查
|
||||
|
||||
```go
|
||||
// POST JSON(最常见)
|
||||
a.JSON(Map{"name": "test", "password": "123"}).Post("描述", 0, Map{...})
|
||||
|
||||
// POST Form 表单
|
||||
a.Form(Map{"shop_id": "1", "id": "5"}).Post("表单提交", 0, Map{...})
|
||||
|
||||
// GET 无参数
|
||||
a.Get("描述", 0, Map{...})
|
||||
|
||||
// GET + URL 参数
|
||||
a.Query(Map{"shop_id": "1", "page": "1"}).Get("查询列表", 0, Map{...})
|
||||
|
||||
// PUT
|
||||
// POST JSON / Form / GET / PUT / DELETE
|
||||
a.JSON(Map{"name": "test"}).Post("描述", 0, Map{...})
|
||||
a.Form(Map{"shop_id": "1"}).Post("表单提交", 0, Map{...})
|
||||
a.Query(Map{"page": "1"}).Get("查询列表", 0, Map{...})
|
||||
a.Get("无参 GET", 0, Map{...})
|
||||
a.JSON(Map{"name": "新名字"}).Put("更新", 0, Map{...})
|
||||
|
||||
// DELETE + URL 参数
|
||||
a.Query(Map{"id": "5"}).Delete("删除", 0, "操作成功")
|
||||
```
|
||||
|
||||
### 登录态
|
||||
// 登录态
|
||||
a.WithSession(Map{"user_id": int64(1)}).JSON(Map{...}).Post("更新", 0, Map{...})
|
||||
|
||||
```go
|
||||
// 需登录 + GET
|
||||
a.WithSession(Map{"user_id": int64(1)}).Get("已登录访问", 0, Map{...})
|
||||
|
||||
// 需登录 + GET + URL 参数
|
||||
a.WithSession(Map{"user_id": int64(1)}).Query(Map{"shop_id": "1"}).Get("带参查询", 0, Map{...})
|
||||
|
||||
// 需登录 + POST JSON
|
||||
a.WithSession(Map{"user_id": int64(1)}).JSON(Map{"name": "新名字"}).Post("更新信息", 0, Map{...})
|
||||
```
|
||||
|
||||
### 混合参数
|
||||
|
||||
```go
|
||||
// URL 参数 + JSON body
|
||||
// 混合参数 / 上传
|
||||
a.Query(Map{"token": "abc"}).JSON(Map{"phone": "138"}).Post("混合传参", 0, Map{...})
|
||||
|
||||
// 文件上传
|
||||
a.File("file", "photo.png", imgBytes).Post("上传文件", 0, Map{...})
|
||||
|
||||
// 文件 + 表单字段
|
||||
a.File("file", "photo.png", imgBytes).Form(Map{"type": "avatar"}).Post("上传头像", 0, Map{...})
|
||||
```
|
||||
|
||||
### Verify 校验
|
||||
|
||||
```go
|
||||
// 纯 GET + Verify(无需 Form/JSON/Query)
|
||||
a.Verify(func(a *Api) error {
|
||||
result := a.Resp().GetBody().GetMap("result")
|
||||
if result.GetString("version") == "" {
|
||||
return fmt.Errorf("缺少 version 字段")
|
||||
}
|
||||
return nil
|
||||
}).Get("获取系统信息", 0, Map{"version": "sample"})
|
||||
|
||||
// Form + Verify + 响应值断言 + DB 校验
|
||||
a.Form(Map{"name": "测试"}).
|
||||
Verify(func(a *Api) error {
|
||||
result := a.Resp().GetBody().GetMap("result")
|
||||
if result.GetCeilInt64("id") <= 0 {
|
||||
return fmt.Errorf("id 无效")
|
||||
}
|
||||
row := a.DB().Get("order", "id", Map{"id": result.GetCeilInt64("id")})
|
||||
if row == nil {
|
||||
return fmt.Errorf("数据库未写入")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Post("创建订单", 0, Map{"id": int64(1), "name": "sample"})
|
||||
```
|
||||
|
||||
### 备注
|
||||
|
||||
```go
|
||||
// 备注显示在调试控制台用例详情中(没有备注时不显示)
|
||||
a.Note("sign = MD5(smsProxyKey+timestamp)").
|
||||
Form(Map{"phone": "138", "code": "1234"}).Post("正常发送", 0, Map{...})
|
||||
// Verify + 备注
|
||||
a.Note("sign = MD5(key+timestamp)").
|
||||
Form(Map{"phone": "138"}).
|
||||
Verify(func(a *Api) error { /* a.Resp() / a.DB() */ return nil }).
|
||||
Post("正常发送", 0, Map{...})
|
||||
```
|
||||
|
||||
---
|
||||
@@ -761,110 +718,6 @@ that.Db.Action(func(db HoTimeDB) bool {
|
||||
|
||||
---
|
||||
|
||||
## 覆盖率报告
|
||||
|
||||
`PrintCoverage()` 自动对比 `Proj`(所有路由)和 `ProjTest`(有测试的路由),在 `TestMain` 中调用:
|
||||
|
||||
```go
|
||||
func TestMain(m *testing.M) {
|
||||
// ...
|
||||
code := m.Run()
|
||||
testApp.PrintCoverage() // 在测试运行完成后输出报告
|
||||
os.Exit(code)
|
||||
}
|
||||
```
|
||||
|
||||
**输出示例(全部通过):**
|
||||
|
||||
```
|
||||
========== API 测试覆盖率报告 ==========
|
||||
总接口: 42 | 已覆盖: 6 | 覆盖率: 14.3%
|
||||
总用例: 16 | 通过: 16 | 未通过: 0 | 通过率: 100.0%
|
||||
|
||||
---------- 通过的接口 ----------
|
||||
✓ /api/user/login 4 用例 (4 通过, 0 失败) 12ms
|
||||
✓ /api/user/info 2 用例 (2 通过, 0 失败) 5ms
|
||||
✓ /api/user/token 2 用例 (2 通过, 0 失败) 4ms
|
||||
✓ /api/user/create 4 用例 (4 通过, 0 失败) 18ms
|
||||
✓ /api/user/forget 2 用例 (2 通过, 0 失败) 6ms
|
||||
✓ /api/user/file 2 用例 (2 通过, 0 失败) 9ms
|
||||
|
||||
未覆盖的接口: (36 个)
|
||||
- api/goods/list
|
||||
- api/goods/create
|
||||
- api/order/list
|
||||
- ...
|
||||
========================================
|
||||
```
|
||||
|
||||
**输出示例(有失败):**
|
||||
|
||||
```
|
||||
========== API 测试覆盖率报告 ==========
|
||||
总接口: 42 | 已覆盖: 6 | 覆盖率: 14.3%
|
||||
总用例: 16 | 通过: 14 | 未通过: 2 | 通过率: 87.5%
|
||||
|
||||
---------- 未通过的接口 (1个) ----------
|
||||
✗ /api/order/create 5 用例 (3 通过, 2 失败) 25ms
|
||||
[失败] 正常创建订单 — Verify: order 表未写入订单记录
|
||||
[失败] 库存扣减 — 状态码不匹配: 期望 0, 实际 4
|
||||
|
||||
---------- 通过的接口 ----------
|
||||
✓ /api/user/login 4 用例 (4 通过, 0 失败) 12ms
|
||||
✓ /api/user/info 2 用例 (2 通过, 0 失败) 5ms
|
||||
...
|
||||
|
||||
未覆盖的接口: (36 个)
|
||||
- api/goods/list
|
||||
- ...
|
||||
========================================
|
||||
```
|
||||
|
||||
报告分为四个区域:
|
||||
- **汇总区**:接口覆盖率 + 用例通过率,一目了然
|
||||
- **未通过的接口**:失败接口单独置顶,每个失败用例附带失败原因(状态码不匹配/消息不匹配/响应结构校验失败/Verify 校验失败)
|
||||
- **通过的接口**:正常通过的接口列表
|
||||
- **未覆盖的接口**:尚未编写测试的接口
|
||||
|
||||
### api-spec.json 覆盖率字段说明
|
||||
|
||||
`GenerateSwagger` 生成的每个 `api-spec.json` 文件中,每条 `endpoint` 记录包含以下与测试状态相关的字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "/api/user/login",
|
||||
"project": "api",
|
||||
"ctr": "user",
|
||||
"method": "POST",
|
||||
"summary": "用户登录",
|
||||
"tested": true,
|
||||
"params": [
|
||||
{ "name": "name", "required": true, "type": "string", "example": "13800138000", "in": "json" },
|
||||
{ "name": "password", "required": false, "type": "string", "example": "123456", "in": "json" }
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"name": "正常登录",
|
||||
"method": "POST",
|
||||
"passed": true,
|
||||
"note": "备注内容(可选,未调用 .Note() 则不出现该字段)",
|
||||
"query": {},
|
||||
"json": { "name": "13800138000", "password": "123456" },
|
||||
"response": { "status": 0, "msg": "ok", "data": {} }
|
||||
},
|
||||
{
|
||||
"name": "密码为空",
|
||||
"method": "POST",
|
||||
"passed": true,
|
||||
"json": { "name": "13800138000", "password": "" },
|
||||
"response": { "status": 4, "msg": "用户名或密码不能为空" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 调试控制台
|
||||
|
||||
`GenerateSwagger` 按模块生成独立的交互式 API 调试控制台(暗色高对比度主题),每个模块一个子目录,支持独立分发。
|
||||
@@ -903,32 +756,6 @@ testApp.GenerateSwagger(
|
||||
|
||||
---
|
||||
|
||||
## 指定运行范围
|
||||
|
||||
`RunTests` 使用 Go 标准 `t.Run()` 子测试机制,天然支持 `-run` 参数过滤:
|
||||
|
||||
```bash
|
||||
# 运行全部测试
|
||||
go test ./app/... -v
|
||||
|
||||
# 只跑 order 控制器的所有接口
|
||||
go test ./app/... -v -run TestApi/app/order
|
||||
|
||||
# 只跑 order/create 接口
|
||||
go test ./app/... -v -run TestApi/app/order/create
|
||||
|
||||
# 只跑 create 接口中"正常创建订单"这一个用例
|
||||
go test ./app/... -v -run "TestApi/app/order/create/正常创建订单"
|
||||
|
||||
# 同时跑 user 和 order 两个控制器
|
||||
go test ./app/... -v -run "TestApi/app/(user|order)"
|
||||
|
||||
# 跑所有控制器中包含"未登录"的用例
|
||||
go test ./app/... -v -run "TestApi/.*/.*未登录"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发保护与缓存隔离
|
||||
|
||||
框架自动处理以下问题,编写测试时无需关心:
|
||||
@@ -1027,3 +854,86 @@ a.Verify(func(a *Api) error {
|
||||
> **注意**:二进制/纯文本响应不走 status 校验(JSON 解析失败后 `Body` 为空 Map,`status` 默认 0),
|
||||
> 所以 `Get("描述", 0)` 只是形式上通过。**真正的校验逻辑必须写在 Verify 中**,对 `RawBody` 做内容断言。
|
||||
|
||||
## 全量回归与覆盖率(仅 CI / 发版前)
|
||||
|
||||
> **本节仅适用于 CI 流水线或发版前人工回归。** 日常开发与 AI 辅助请使用文首的 `-run` 单测命令,不要执行本节命令。
|
||||
|
||||
### 全量命令
|
||||
|
||||
```bash
|
||||
# 跑完当前包(或 ./app/...)下全部 API 测试——慢,且任一无关失败都会打断你
|
||||
go test ./app/... -v
|
||||
```
|
||||
|
||||
`TestMain` 里常见的收尾逻辑(全量跑完后才有意义):
|
||||
|
||||
```go
|
||||
func TestMain(m *testing.M) {
|
||||
// ...
|
||||
code := m.Run()
|
||||
testApp.PrintCoverage() // 全量跑完后输出覆盖率
|
||||
testApp.GenerateSwagger("My API", "1.0.0", testApp.Config.GetString("tpt"))
|
||||
os.Exit(code)
|
||||
}
|
||||
```
|
||||
|
||||
### 覆盖率报告
|
||||
|
||||
`PrintCoverage()` 自动对比 `Proj`(所有路由)和 `ProjTest`(有测试的路由):
|
||||
|
||||
**输出示例(全部通过):**
|
||||
|
||||
```
|
||||
========== API 测试覆盖率报告 ==========
|
||||
总接口: 42 | 已覆盖: 6 | 覆盖率: 14.3%
|
||||
总用例: 16 | 通过: 16 | 未通过: 0 | 通过率: 100.0%
|
||||
|
||||
---------- 通过的接口 ----------
|
||||
✓ /api/user/login 4 用例 (4 通过, 0 失败) 12ms
|
||||
...
|
||||
|
||||
未覆盖的接口: (36 个)
|
||||
- api/goods/list
|
||||
- ...
|
||||
========================================
|
||||
```
|
||||
|
||||
**输出示例(有失败):**
|
||||
|
||||
```
|
||||
========== API 测试覆盖率报告 ==========
|
||||
总接口: 42 | 已覆盖: 6 | 覆盖率: 14.3%
|
||||
总用例: 16 | 通过: 14 | 未通过: 2 | 通过率: 87.5%
|
||||
|
||||
---------- 未通过的接口 (1个) ----------
|
||||
✗ /api/order/create 5 用例 (3 通过, 2 失败) 25ms
|
||||
[失败] 正常创建订单 — Verify: order 表未写入订单记录
|
||||
...
|
||||
========================================
|
||||
```
|
||||
|
||||
报告分为四个区域:
|
||||
- **汇总区**:接口覆盖率 + 用例通过率
|
||||
- **未通过的接口**:失败接口单独置顶,附带失败原因
|
||||
- **通过的接口**:正常通过的接口列表
|
||||
- **未覆盖的接口**:尚未编写测试的接口
|
||||
|
||||
### api-spec.json 覆盖率字段说明
|
||||
|
||||
`GenerateSwagger` 生成的每个 `api-spec.json` 中,每条 `endpoint` 可含:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "/api/user/login",
|
||||
"tested": true,
|
||||
"cases": [
|
||||
{
|
||||
"name": "正常登录",
|
||||
"passed": true,
|
||||
"note": "备注(可选)",
|
||||
"json": { "name": "13800138000", "password": "123456" },
|
||||
"response": { "status": 0, "msg": "ok", "data": {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user