Files
junhong_cmp_fiber/docs/testing/test-connection-guide.md
huang 79c061b6fa
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
- 新增套餐系列管理 (CRUD + 状态切换)
- 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态)
- 清理 8 个废弃分佣模型及对应数据库表
- Package 模型新增建议成本价、建议售价、上架状态字段
- 完整的 Store/Service/Handler 三层实现
- 包含单元测试和集成测试
- 归档 add-package-module change
- 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
2026-01-27 19:55:47 +08:00

285 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试数据库连接管理规范
本文档是测试连接管理的**唯一标准**,所有新测试必须遵循此规范。
## ⚠️ 运行测试前必须加载环境变量
**所有测试命令必须先加载 `.env.local` 环境变量**,否则测试将因缺少数据库/Redis 配置而失败。
### 命令格式
```bash
# ✅ 正确:先 source 环境变量
source .env.local && go test -v ./internal/service/xxx/...
# ✅ 正确:运行所有测试
source .env.local && go test ./...
# ❌ 错误:直接运行测试(会因缺少配置而失败)
go test -v ./internal/service/xxx/...
```
### 环境变量文件
- **`.env.local`**: 本地开发/测试环境配置(不提交到 Git
- 包含数据库连接、Redis 地址、JWT 密钥等必要配置
- 如果文件不存在,从 `.env.example` 复制并填写实际值
### 常见错误
如果看到以下错误,说明未加载环境变量:
```
--- SKIP: TestXxx (0.00s)
test_helpers.go:xx: 跳过测试:无法连接测试数据库
```
或:
```
panic: 配置加载失败: 缺少必要的数据库配置
```
**解决方案**:确保运行 `source .env.local` 后再执行测试。
---
## 快速开始
```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. 验证键名是否包含测试名称前缀