All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15s
- 创建全局单例连接池,性能提升 6-7 倍 - 实现 NewTestTransaction/GetTestRedis/CleanTestRedisKeys - 移除旧的 SetupTestDB/TeardownTestDB API - 迁移所有测试文件到新方案(47 个文件) - 添加测试连接管理规范文档 - 更新 AGENTS.md 和 README.md 性能对比: - 旧方案:~71 秒(204 测试) - 新方案:~10.5 秒(首次初始化 + 后续复用) - 内存占用降低约 80% - 网络连接数从 204 降至 1
243 lines
5.5 KiB
Markdown
243 lines
5.5 KiB
Markdown
# 测试数据库连接管理规范
|
|
|
|
本文档是测试连接管理的**唯一标准**,所有新测试必须遵循此规范。
|
|
|
|
## 快速开始
|
|
|
|
```go
|
|
func TestXxx(t *testing.T) {
|
|
tx := testutils.NewTestTransaction(t)
|
|
rdb := testutils.GetTestRedis(t)
|
|
testutils.CleanTestRedisKeys(t, rdb)
|
|
|
|
store := postgres.NewXxxStore(tx, rdb)
|
|
// 测试代码...
|
|
// 测试结束后自动回滚,无需手动清理
|
|
}
|
|
```
|
|
|
|
## 核心 API
|
|
|
|
### NewTestTransaction(t) - 创建测试事务
|
|
|
|
```go
|
|
func NewTestTransaction(t *testing.T) *gorm.DB
|
|
```
|
|
|
|
- 返回独立事务,测试结束后自动回滚
|
|
- 使用 `t.Cleanup()` 确保即使 panic 也能清理
|
|
- **所有数据库操作都应使用此函数返回的 tx**
|
|
|
|
### GetTestDB(t) - 获取全局数据库连接
|
|
|
|
```go
|
|
func GetTestDB(t *testing.T) *gorm.DB
|
|
```
|
|
|
|
- 全局单例,整个测试套件只创建一次
|
|
- AutoMigrate 只在首次调用时执行
|
|
- 通常不直接使用,而是通过 `NewTestTransaction` 间接使用
|
|
|
|
### GetTestRedis(t) - 获取全局 Redis 连接
|
|
|
|
```go
|
|
func GetTestRedis(t *testing.T) *redis.Client
|
|
```
|
|
|
|
- 全局单例,复用连接
|
|
- 需配合 `CleanTestRedisKeys` 使用以避免键污染
|
|
|
|
### CleanTestRedisKeys(t, rdb) - 清理测试 Redis 键
|
|
|
|
```go
|
|
func CleanTestRedisKeys(t *testing.T, rdb *redis.Client)
|
|
```
|
|
|
|
- 测试开始前清理已有键
|
|
- 测试结束后自动清理
|
|
- 键前缀格式: `test:{TestName}:*`
|
|
|
|
### GetTestRedisKeyPrefix(t) - 获取 Redis 键前缀
|
|
|
|
```go
|
|
func GetTestRedisKeyPrefix(t *testing.T) string
|
|
```
|
|
|
|
- 返回格式: `test:{TestName}:`
|
|
- 用于在测试中创建带前缀的键
|
|
|
|
## 使用示例
|
|
|
|
### 基础单元测试
|
|
|
|
```go
|
|
func TestUserStore_Create(t *testing.T) {
|
|
tx := testutils.NewTestTransaction(t)
|
|
rdb := testutils.GetTestRedis(t)
|
|
testutils.CleanTestRedisKeys(t, rdb)
|
|
|
|
store := postgres.NewUserStore(tx, rdb)
|
|
ctx := context.Background()
|
|
|
|
user := &model.User{Name: "测试用户"}
|
|
err := store.Create(ctx, user)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, user.ID)
|
|
}
|
|
```
|
|
|
|
### Table-Driven Tests
|
|
|
|
```go
|
|
func TestUserStore_Validate(t *testing.T) {
|
|
tx := testutils.NewTestTransaction(t)
|
|
rdb := testutils.GetTestRedis(t)
|
|
testutils.CleanTestRedisKeys(t, rdb)
|
|
|
|
store := postgres.NewUserStore(tx, rdb)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
user *model.User
|
|
wantErr bool
|
|
}{
|
|
{"有效用户", &model.User{Name: "张三"}, false},
|
|
{"空名称", &model.User{Name: ""}, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := store.Create(ctx, tt.user)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### 需要 Redis 操作的测试
|
|
|
|
```go
|
|
func TestCacheStore_Get(t *testing.T) {
|
|
tx := testutils.NewTestTransaction(t)
|
|
rdb := testutils.GetTestRedis(t)
|
|
testutils.CleanTestRedisKeys(t, rdb)
|
|
|
|
ctx := context.Background()
|
|
prefix := testutils.GetTestRedisKeyPrefix(t)
|
|
|
|
// 使用测试前缀创建键
|
|
key := prefix + "user:1"
|
|
rdb.Set(ctx, key, "cached_data", time.Hour)
|
|
|
|
// 验证
|
|
val, err := rdb.Get(ctx, key).Result()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "cached_data", val)
|
|
// 测试结束后自动清理 key
|
|
}
|
|
```
|
|
|
|
## 常见陷阱
|
|
|
|
### 1. 子测试中不要调用 NewTestTransaction
|
|
|
|
❌ 错误:
|
|
```go
|
|
func TestXxx(t *testing.T) {
|
|
t.Run("子测试", func(t *testing.T) {
|
|
tx := testutils.NewTestTransaction(t) // 错误!
|
|
})
|
|
}
|
|
```
|
|
|
|
✅ 正确:
|
|
```go
|
|
func TestXxx(t *testing.T) {
|
|
tx := testutils.NewTestTransaction(t) // 在父测试中创建
|
|
|
|
t.Run("子测试1", func(t *testing.T) {
|
|
// 使用父测试的 tx
|
|
})
|
|
t.Run("子测试2", func(t *testing.T) {
|
|
// 共享同一个 tx
|
|
})
|
|
}
|
|
```
|
|
|
|
### 2. 不要在测试中调用 db.Close()
|
|
|
|
事务和连接由框架管理,不要手动关闭。
|
|
|
|
### 3. 并发测试需要独立事务
|
|
|
|
如果使用 `t.Parallel()`,每个测试必须有独立的事务:
|
|
|
|
```go
|
|
func TestConcurrent(t *testing.T) {
|
|
t.Run("test1", func(t *testing.T) {
|
|
t.Parallel()
|
|
tx := testutils.NewTestTransaction(t) // 每个并发测试独立事务
|
|
// ...
|
|
})
|
|
t.Run("test2", func(t *testing.T) {
|
|
t.Parallel()
|
|
tx := testutils.NewTestTransaction(t)
|
|
// ...
|
|
})
|
|
}
|
|
```
|
|
|
|
## 性能对比
|
|
|
|
| 指标 | 旧方案 (SetupTestDB) | 新方案 (NewTestTransaction) |
|
|
|------|---------------------|----------------------------|
|
|
| 单测平均耗时 | ~350ms | ~50ms |
|
|
| 204 个测试总耗时 | ~71 秒 | ~10.5 秒 |
|
|
| 数据库连接数 | 204 个 | 1 个 |
|
|
| 内存占用 | 高 | 低 (降低 ~80%) |
|
|
|
|
## 从旧 API 迁移
|
|
|
|
旧方式:
|
|
```go
|
|
db, rdb := testutils.SetupTestDB(t)
|
|
defer testutils.TeardownTestDB(t, db, rdb)
|
|
store := postgres.NewXxxStore(db, rdb)
|
|
```
|
|
|
|
新方式:
|
|
```go
|
|
tx := testutils.NewTestTransaction(t)
|
|
rdb := testutils.GetTestRedis(t)
|
|
testutils.CleanTestRedisKeys(t, rdb)
|
|
store := postgres.NewXxxStore(tx, rdb)
|
|
```
|
|
|
|
## 故障排查
|
|
|
|
### 连接超时
|
|
|
|
如果测试跳过并显示"无法连接测试数据库":
|
|
1. 检查网络连接
|
|
2. 验证数据库服务状态
|
|
3. 确认 DSN 配置正确
|
|
|
|
### 事务死锁
|
|
|
|
如果测试卡住:
|
|
1. 检查是否有未提交的长事务
|
|
2. 避免在单个测试中执行耗时操作
|
|
3. 确保测试 < 1 秒完成
|
|
|
|
### Redis 键冲突
|
|
|
|
如果出现数据污染:
|
|
1. 确保使用 `CleanTestRedisKeys`
|
|
2. 检查是否正确使用 `GetTestRedisKeyPrefix`
|
|
3. 验证键名是否包含测试名称前缀
|