refactor(db): 增强测试事务支持和保存点管理
- 在 HoTimeDB 中新增测试事务功能,允许在测试模式下使用事务进行操作 - 实现 BeginTestTx 和 RollbackTestTx 方法,支持测试事务的开启和回滚 - 在 Action 方法中集成保存点管理,确保在测试模式下的嵌套事务处理 - 更新 README.md,添加 API 测试框架的相关说明,提升文档完整性
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
# HoTime API 测试框架使用说明
|
||||
|
||||
不启动 HTTP 服务、自动事务回滚、链式 API、覆盖率报告、API 调试控制台生成。
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [快速开始](#快速开始)
|
||||
- [核心类型](#核心类型)
|
||||
- [链式 API 参考](#链式-api-参考)
|
||||
- [完整场景对照](#完整场景对照)
|
||||
- [事务隔离机制](#事务隔离机制)
|
||||
- [覆盖率报告](#覆盖率报告)
|
||||
- [API 调试控制台](#api-调试控制台)
|
||||
- [指定运行范围](#指定运行范围)
|
||||
- [框架改动说明](#框架改动说明)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
HoTime 内置 API 测试框架,核心能力:
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **零启动** | 基于 `net/http/httptest`,不需要启动 HTTP 服务器 |
|
||||
| **事务回滚** | 每个接口方法独立开启数据库事务,测试结束自动回滚,数据库不留任何痕迹 |
|
||||
| **SAVEPOINT** | 业务代码中的 `that.Db.Action()` 在测试模式下自动用 SAVEPOINT 替代真事务,嵌套事务完整可用 |
|
||||
| **链式 API** | `a.JSON(...).Post("描述", 期望status)` 一行完成:数据组装 + 请求 + 断言 |
|
||||
| **覆盖率** | 自动对比路由注册表与测试定义,输出哪些接口已覆盖、哪些未覆盖 |
|
||||
| **调试控制台** | 根据测试用例自动生成交互式 API 调试控制台,支持在线测试、认证管理、参数编辑 |
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 第一步:创建测试定义文件(推荐)
|
||||
|
||||
**推荐方式**:为每个 handler 创建对应的 `_test.go` 文件,测试定义与业务代码分离。`_test.go` 文件仅在 `go test` 时编译,不影响生产构建。
|
||||
|
||||
```go
|
||||
// app/user.go — 只有业务代码
|
||||
package app
|
||||
|
||||
import . "code.hoteas.com/golang/hotime"
|
||||
import . "code.hoteas.com/golang/hotime/common"
|
||||
|
||||
var UserCtr = Ctr{
|
||||
"login": func(that *Context) { /* ... */ },
|
||||
"info": func(that *Context) { /* ... */ },
|
||||
"create": func(that *Context) { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// app/user_test.go — 测试定义
|
||||
package app
|
||||
|
||||
import . "code.hoteas.com/golang/hotime"
|
||||
import . "code.hoteas.com/golang/hotime/common"
|
||||
|
||||
var UserTest = CtrTest{
|
||||
"login": {Desc: "用户登录", Func: 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, "手机号码格式错误")
|
||||
}},
|
||||
"create": {Desc: "用户注册", Func: func(a *Api) {
|
||||
phone := "138" + ObjToStr(RandX(10000000, 99999999))
|
||||
a.JSON(Map{"phone": phone, "password": "123456"}).Post("正常注册", 0)
|
||||
a.JSON(Map{"phone": phone, "password": "123456"}).Post("重复注册", 3, "该手机号已被注册")
|
||||
}},
|
||||
}
|
||||
```
|
||||
|
||||
> **为什么要分离?** `_test.go` 文件仅在 `go test` 时编译,不会进入生产二进制,保持业务代码干净。同时符合 Go 语言惯例:测试相关代码放在 `_test.go` 文件中。
|
||||
|
||||
### 第二步:注册路由和测试
|
||||
|
||||
路由在 `init.go` 中注册,测试注册放在 `app_test.go` 中(因为 `_test.go` 中的变量只在测试编译时可见):
|
||||
|
||||
```go
|
||||
// app/init.go — 只注册路由
|
||||
var Project = Proj{
|
||||
"user": UserCtr,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// app/app_test.go — 注册测试 + 运行入口
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
. "code.hoteas.com/golang/hotime"
|
||||
)
|
||||
|
||||
var ProjectTest = ProjTest{
|
||||
"user": UserTest,
|
||||
}
|
||||
|
||||
var testApp *TestApp
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Chdir("..") // 确保工作目录为项目根目录
|
||||
testApp = NewTestApp("config/config.json", TestProj{
|
||||
"app": {Proj: Project, Tests: ProjectTest},
|
||||
})
|
||||
code := m.Run()
|
||||
testApp.PrintCoverage()
|
||||
testApp.GenerateSwagger("My API", "1.0.0", testApp.Config.GetString("tpt"))
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
testApp.RunTests(t)
|
||||
}
|
||||
```
|
||||
|
||||
### 推荐目录结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── user.go ← 业务代码 (UserCtr)
|
||||
├── user_test.go ← 测试定义 (UserTest)
|
||||
├── tags.go ← 业务代码 (TagsCtr)
|
||||
├── tags_test.go ← 测试定义 (TagsTest)
|
||||
├── init.go ← 路由注册 (Project)
|
||||
└── app_test.go ← 测试注册 (ProjectTest) + TestMain + TestApi
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
go test ./app/... -v
|
||||
```
|
||||
|
||||
> **注意**:`TestMain` 中的 `os.Chdir("..")` 确保 `go test ./app/` 的工作目录为项目根目录,使配置文件路径和模板输出路径正确。
|
||||
|
||||
---
|
||||
|
||||
## 核心类型
|
||||
|
||||
### 注册类型
|
||||
|
||||
```go
|
||||
// 顶层:多项目集合(项目名 → 定义)
|
||||
type TestProj map[string]TestProjDef
|
||||
|
||||
// 单项目:路由 + 测试绑定在一起
|
||||
type TestProjDef struct {
|
||||
Proj Proj // 路由定义(与 Run 传入的 Router 保持一致)
|
||||
Tests ProjTest // 测试定义
|
||||
}
|
||||
|
||||
// 控制器级测试(控制器名 → 控制器测试集合)
|
||||
type ProjTest map[string]CtrTest
|
||||
|
||||
// 方法级测试(方法名 → 单条用例定义)
|
||||
type CtrTest map[string]ApiTestDef
|
||||
|
||||
// 单个接口的用例定义
|
||||
type ApiTestDef struct {
|
||||
Desc string // 接口描述,用于 Swagger 说明和 t.Run 层级名称
|
||||
Func func(a *Api) // 测试函数体
|
||||
}
|
||||
```
|
||||
|
||||
### TestApp
|
||||
|
||||
```go
|
||||
// NewTestApp 创建测试应用
|
||||
// configPath: 配置文件路径
|
||||
// projects: TestProj 注册所有项目的路由和测试
|
||||
// listeners: 可选,与 SetConnectListener 相同的请求拦截器
|
||||
func NewTestApp(configPath string, projects TestProj, listeners ...func(*Context) bool) *TestApp
|
||||
|
||||
// RunTests 运行所有注册的测试(每个方法级别自动事务隔离)
|
||||
func (app *TestApp) RunTests(t *testing.T)
|
||||
|
||||
// PrintCoverage 输出覆盖率报告,返回报告结构体
|
||||
func (app *TestApp) PrintCoverage() CoverageReport
|
||||
|
||||
// GenerateSwagger 生成 API 调试控制台(api-spec.json + index.html)
|
||||
func (app *TestApp) GenerateSwagger(title, version, outputDir string) error
|
||||
|
||||
// DB 获取测试应用的数据库实例(在事务内)
|
||||
func (app *TestApp) DB() *HoTimeDB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 链式 API 参考
|
||||
|
||||
测试函数接收 `*Api` 作为入口,调用数据设置方法返回 `*ApiCase`,再调用终端方法发出请求并断言。
|
||||
|
||||
### Api — 测试入口
|
||||
|
||||
| 方法 | 说明 | 返回 |
|
||||
|------|------|------|
|
||||
| `a.JSON(body)` | 设置 JSON body | `*ApiCase` |
|
||||
| `a.Query(params)` | 设置 URL 查询参数 | `*ApiCase` |
|
||||
| `a.Form(body)` | 设置 form 表单 body | `*ApiCase` |
|
||||
| `a.File(field, name, content)` | 设置上传文件 | `*ApiCase` |
|
||||
| `a.WithSession(s)` | 返回携带 session 的新 Api | `*Api` |
|
||||
| `a.Get(desc, status, msg...)` | 直接 GET 请求(无数据场景) | `*ApiResponse` |
|
||||
| `a.Post(desc, status, msg...)` | 直接 POST 请求(无数据场景) | `*ApiResponse` |
|
||||
| `a.Put(desc, status, msg...)` | 直接 PUT 请求 | `*ApiResponse` |
|
||||
| `a.Delete(desc, status, msg...)` | 直接 DELETE 请求 | `*ApiResponse` |
|
||||
| `a.DB()` | 获取数据库实例(在事务内,可用于准备/清查数据) | `*HoTimeDB` |
|
||||
|
||||
### ApiCase — 链式构建器
|
||||
|
||||
`Api` 的数据设置方法返回 `*ApiCase`,支持继续叠加数据:
|
||||
|
||||
| 方法 | 说明 | 返回 |
|
||||
|------|------|------|
|
||||
| `c.JSON(body)` | 追加 JSON body | `*ApiCase` |
|
||||
| `c.Query(params)` | 追加 URL 查询参数 | `*ApiCase` |
|
||||
| `c.Form(body)` | 追加 form 字段 | `*ApiCase` |
|
||||
| `c.File(field, name, content)` | 设置上传文件 | `*ApiCase` |
|
||||
| `c.WithSession(s)` | 覆盖本次请求的 session | `*ApiCase` |
|
||||
| `c.Get(desc, status, msg...)` | 终端:发送 GET 请求并断言 | `*ApiResponse` |
|
||||
| `c.Post(desc, status, msg...)` | 终端:发送 POST 请求并断言 | `*ApiResponse` |
|
||||
| `c.Put(desc, status, msg...)` | 终端:发送 PUT 请求并断言 | `*ApiResponse` |
|
||||
| `c.Delete(desc, status, msg...)` | 终端:发送 DELETE 请求并断言 | `*ApiResponse` |
|
||||
|
||||
**终端方法参数说明:**
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `desc` | string | 用例描述,作为 `t.Run` 的子测试名称 |
|
||||
| `status` | int | 期望的 JSON 响应体中 `status` 字段值(0=成功) |
|
||||
| `msg` | ...string | 可选,期望的 `msg` 字段值(传空则不校验) |
|
||||
|
||||
### ApiResponse — 响应对象
|
||||
|
||||
终端方法返回 `*ApiResponse`,可用于进一步的自定义断言:
|
||||
|
||||
```go
|
||||
resp := a.JSON(Map{"name": "user", "password": "123"}).Post("正常登录", 0)
|
||||
|
||||
// 获取响应字段
|
||||
resp.GetStatus() // int:业务 status
|
||||
resp.GetMsg() // string:业务 msg
|
||||
resp.GetResult() // interface{}:result 字段
|
||||
resp.GetBody() // Map:完整响应体
|
||||
|
||||
// 自定义断言
|
||||
token := resp.GetBody().GetMap("result").GetString("token")
|
||||
if token == "" {
|
||||
resp.Fail("缺少 token 字段")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整场景对照
|
||||
|
||||
以下示例均写在 `_test.go` 文件的 `CtrTest` 定义中(如 `user_test.go`),`a` 为 `*Api` 参数:
|
||||
|
||||
```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 + URL 参数
|
||||
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": "新名字"}).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": "5"}).Post("表单提交", 0)
|
||||
|
||||
// 文件上传
|
||||
a.File("file", "photo.png", imgBytes).Post("上传文件", 0)
|
||||
|
||||
// 文件 + 表单字段混合
|
||||
a.File("file", "photo.png", imgBytes).Form(Map{"type": "avatar"}).Post("上传头像", 0)
|
||||
|
||||
// PUT
|
||||
a.JSON(Map{"name": "新名字"}).Put("更新", 0)
|
||||
|
||||
// DELETE + URL 参数
|
||||
a.Query(Map{"id": "5"}).Delete("删除", 0)
|
||||
|
||||
// 校验响应中的具体字段
|
||||
resp := a.JSON(Map{"name": "138", "password": "123"}).Post("正常登录", 0)
|
||||
if resp.GetBody().GetMap("result").GetString("token") == "" {
|
||||
resp.Fail("响应中缺少 token 字段")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 事务隔离机制
|
||||
|
||||
### 工作原理
|
||||
|
||||
`RunTests` 在每个**方法级**测试前开启一个数据库事务(`testTx`),测试结束后无论成功与否一律回滚:
|
||||
|
||||
```
|
||||
TestApi
|
||||
└── app(项目名)
|
||||
└── user(控制器名)
|
||||
└── create(方法名)← 在这一级 BEGIN → 运行测试用例 → ROLLBACK
|
||||
└── 正常注册(用例)
|
||||
└── 重复注册(用例)
|
||||
```
|
||||
|
||||
所有测试用例共享同一个 `testTx`,因此"正常注册"写入的数据对"重复注册"可见,模拟了真实的并发隔离场景。
|
||||
|
||||
### 嵌套事务 (SAVEPOINT)
|
||||
|
||||
业务代码中常见的 `that.Db.Action()` 在测试模式下自动切换为 SAVEPOINT 机制,不会脱离外层测试事务:
|
||||
|
||||
```go
|
||||
// 业务代码(无需修改)
|
||||
that.Db.Action(func(db HoTimeDB) bool {
|
||||
db.Insert("order", orderData) // 在 SAVEPOINT sp_N 内执行
|
||||
db.Update("customer", balanceData, where)
|
||||
return true // → RELEASE SAVEPOINT sp_N(提交到外层 testTx)
|
||||
// 返回 false → ROLLBACK TO SAVEPOINT sp_N(只回滚这一层)
|
||||
})
|
||||
// 测试结束 → ROLLBACK testTx(回滚一切)
|
||||
```
|
||||
|
||||
| 机制 | 生产模式 | 测试模式 |
|
||||
|------|----------|----------|
|
||||
| `Action()` | `BEGIN` / `COMMIT` / `ROLLBACK` | `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` |
|
||||
| 数据持久化 | 持久化到数据库 | 测试结束全部回滚 |
|
||||
| 对现有代码影响 | 无 | 无(`testTx` 默认 nil) |
|
||||
|
||||
### 一次性数据的处理
|
||||
|
||||
对于需要随机数据的测试(如注册),直接用 `RandX` 生成随机值即可,**无需手动清理**:
|
||||
|
||||
```go
|
||||
// app/user_test.go 中的 UserTest 片段
|
||||
"create": {Desc: "用户注册", Func: func(a *Api) {
|
||||
// 生成随机手机号,测试完成后事务自动回滚,数据库中不留任何记录
|
||||
phone := "138" + ObjToStr(RandX(10000000, 99999999))
|
||||
a.JSON(Map{"phone": phone, "password": "123456"}).Post("正常注册", 0)
|
||||
a.JSON(Map{"phone": phone, "password": "123456"}).Post("重复注册", 3, "该手机号已被注册")
|
||||
}},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 覆盖率报告
|
||||
|
||||
`PrintCoverage()` 自动对比 `Proj`(所有路由)和 `ProjTest`(有测试的路由),在 `TestMain` 中调用:
|
||||
|
||||
```go
|
||||
func TestMain(m *testing.M) {
|
||||
// ...
|
||||
code := m.Run()
|
||||
testApp.PrintCoverage() // 在测试运行完成后输出报告
|
||||
os.Exit(code)
|
||||
}
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
|
||||
```
|
||||
========== API 测试覆盖率报告 ==========
|
||||
总接口: 42 | 已覆盖: 6 | 覆盖率: 14.3%
|
||||
|
||||
已覆盖的接口:
|
||||
+ /app/user/login 4 用例 (4 通过, 0 失败) 12ms
|
||||
+ /app/user/info 2 用例 (2 通过, 0 失败) 5ms
|
||||
+ /app/user/token 2 用例 (2 通过, 0 失败) 4ms
|
||||
+ /app/user/create 4 用例 (4 通过, 0 失败) 18ms
|
||||
+ /app/user/forget 2 用例 (2 通过, 0 失败) 6ms
|
||||
+ /app/user/file 2 用例 (2 通过, 0 失败) 9ms
|
||||
|
||||
未覆盖的接口:
|
||||
- app/goods/list
|
||||
- app/goods/create
|
||||
- app/order/list
|
||||
- ... (共36个未覆盖)
|
||||
========================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 调试控制台
|
||||
|
||||
`GenerateSwagger` 遍历 `TestProj` 中所有项目的路由和测试用例,生成一个**交互式 API 调试控制台**(暗色高对比度主题),支持在线调试、参数编辑、认证管理。
|
||||
|
||||
```go
|
||||
testApp.GenerateSwagger(
|
||||
"My API", // 文档标题
|
||||
"2.1.0", // 版本号
|
||||
testApp.Config.GetString("tpt"), // 输出目录(如 "tpt")
|
||||
)
|
||||
```
|
||||
|
||||
生成文件:
|
||||
|
||||
```
|
||||
{outputDir}/swagger/
|
||||
├── api-spec.json ← 自定义 API 规范文件(包含路由、测试用例、请求/响应数据)
|
||||
└── index.html ← 交互式调试控制台
|
||||
```
|
||||
|
||||
访问地址:`http://localhost:8081/swagger/`
|
||||
|
||||
### 控制台功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **三级导航** | 左侧侧边栏按 项目 > 控制器 > 方法 三级树形展开 |
|
||||
| **搜索和筛选** | 支持关键字搜索,按 全部/已测试/未测试 筛选 |
|
||||
| **测试用例查看** | 每个接口的测试用例以手风琴形式展示(第一个默认展开),包含请求头、请求参数(Query/JSON/Form/文件)、响应结果 |
|
||||
| **接口调试** | 可编辑的请求头、Query 参数、文件上传、请求体(表单/JSON 互斥切换),直接发送请求 |
|
||||
| **预填用例** | 下拉选择测试用例一键填充请求参数,方便快速调试 |
|
||||
| **全局认证** | 顶部认证栏支持四种模式:无认证、Header (Authorization)、URL (?token=)、Cookie,修改后实时同步到各参数区域 |
|
||||
| **Cookie 隔离** | Header/URL 认证模式下自动阻止浏览器发送 Cookie(`credentials: 'omit'`),避免意外自动授权 |
|
||||
| **cURL 生成** | 每次请求自动生成对应的 cURL 命令,支持一键复制 |
|
||||
| **响应格式化** | JSON 响应自动格式化显示,支持复制 |
|
||||
| **覆盖率可视化** | 已测试接口显示通过/失败计数徽章,未测试接口标记"未测试" |
|
||||
|
||||
### 多项目支持
|
||||
|
||||
所有注册项目的路由和测试用例合并到同一份 `api-spec.json`,在侧边栏按项目名分组,不会互相覆盖:
|
||||
|
||||
```
|
||||
侧边栏结构:
|
||||
├── app(项目)
|
||||
│ ├── user(控制器)
|
||||
│ │ ├── POST login 用户登录 ✅ 4/4
|
||||
│ │ ├── POST info 未测试
|
||||
│ │ └── POST create 用户注册 ✅ 4/4
|
||||
│ └── goods(控制器)
|
||||
│ └── ...
|
||||
└── customer(项目)
|
||||
└── pay(控制器)
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 指定运行范围
|
||||
|
||||
`RunTests` 使用 Go 标准 `t.Run()` 子测试机制,天然支持 `-run` 参数过滤:
|
||||
|
||||
```bash
|
||||
# 运行全部测试
|
||||
go test ./app/... -v
|
||||
|
||||
# 只跑 user 控制器的所有接口
|
||||
go test ./app/... -v -run TestApi/app/user
|
||||
|
||||
# 只跑 user/login 接口
|
||||
go test ./app/... -v -run TestApi/app/user/login
|
||||
|
||||
# 只跑 login 接口中"密码为空"这一个用例
|
||||
go test ./app/... -v -run "TestApi/app/user/login/密码为空"
|
||||
|
||||
# 同时跑 user 和 goods 两个控制器
|
||||
go test ./app/... -v -run "TestApi/app/(user|goods)"
|
||||
|
||||
# 跑所有控制器中包含"未登录"的用例
|
||||
go test ./app/... -v -run "TestApi/.*/.*未登录"
|
||||
```
|
||||
|
||||
子测试层级结构:
|
||||
|
||||
```
|
||||
TestApi
|
||||
└── {项目名} ← -run TestApi/app
|
||||
└── {控制器名} ← -run TestApi/app/user
|
||||
└── {方法名} ← -run TestApi/app/user/login
|
||||
└── {ApiTestDef.Desc}(接口描述)
|
||||
└── {用例描述} ← -run TestApi/app/user/login/密码为空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 框架改动说明
|
||||
|
||||
测试框架对 `hotimev1.5` 共改动 6 个文件,其中 3 个为修改、3 个为新增:
|
||||
|
||||
### 修改的文件
|
||||
|
||||
| 文件 | 改动 | 生产影响 |
|
||||
|------|------|----------|
|
||||
| `db/db.go` | 新增 `testTx *sql.Tx` 字段 + `BeginTestTx()`/`RollbackTestTx()` 方法 | 无(`testTx` 默认 nil) |
|
||||
| `db/query.go` | `Query` 和 `Exec` 各增加一个 `if testTx != nil` 分支 | 无(分支默认跳过) |
|
||||
| `db/transaction.go` | `Action()` 增加 SAVEPOINT 分支 | 无(`testTx` 为 nil 时走原有逻辑) |
|
||||
|
||||
### 新增的文件
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `testing_helper.go` | `TestApp`、`NewTestApp`、`SetupForTest`、`RunTests`(事务隔离)、`PrintCoverage`(覆盖率) |
|
||||
| `testing_api.go` | 注册类型 + `Api`/`ApiCase`/`ApiResponse` 链式 API + `TestCollector` |
|
||||
| `testing_swagger.go` | `GenerateSwagger`:合并所有项目生成 API 调试控制台(`api-spec.json` + `index.html`) |
|
||||
|
||||
> 所有新增文件仅在 `go test` 环境下被使用,生产构建不引入 `testing` 包依赖,完全不影响线上服务。
|
||||
Reference in New Issue
Block a user