All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
- 新增套餐系列管理 (CRUD + 状态切换) - 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态) - 清理 8 个废弃分佣模型及对应数据库表 - Package 模型新增建议成本价、建议售价、上架状态字段 - 完整的 Store/Service/Handler 三层实现 - 包含单元测试和集成测试 - 归档 add-package-module change - 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
6.5 KiB
6.5 KiB
测试数据库连接管理规范
本文档是测试连接管理的唯一标准,所有新测试必须遵循此规范。
⚠️ 运行测试前必须加载环境变量
所有测试命令必须先加载 .env.local 环境变量,否则测试将因缺少数据库/Redis 配置而失败。
命令格式
# ✅ 正确:先 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 后再执行测试。
快速开始
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) - 创建测试事务
func NewTestTransaction(t *testing.T) *gorm.DB
- 返回独立事务,测试结束后自动回滚
- 使用
t.Cleanup()确保即使 panic 也能清理 - 所有数据库操作都应使用此函数返回的 tx
GetTestDB(t) - 获取全局数据库连接
func GetTestDB(t *testing.T) *gorm.DB
- 全局单例,整个测试套件只创建一次
- AutoMigrate 只在首次调用时执行
- 通常不直接使用,而是通过
NewTestTransaction间接使用
GetTestRedis(t) - 获取全局 Redis 连接
func GetTestRedis(t *testing.T) *redis.Client
- 全局单例,复用连接
- 需配合
CleanTestRedisKeys使用以避免键污染
CleanTestRedisKeys(t, rdb) - 清理测试 Redis 键
func CleanTestRedisKeys(t *testing.T, rdb *redis.Client)
- 测试开始前清理已有键
- 测试结束后自动清理
- 键前缀格式:
test:{TestName}:*
GetTestRedisKeyPrefix(t) - 获取 Redis 键前缀
func GetTestRedisKeyPrefix(t *testing.T) string
- 返回格式:
test:{TestName}: - 用于在测试中创建带前缀的键
使用示例
基础单元测试
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
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 操作的测试
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
❌ 错误:
func TestXxx(t *testing.T) {
t.Run("子测试", func(t *testing.T) {
tx := testutils.NewTestTransaction(t) // 错误!
})
}
✅ 正确:
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(),每个测试必须有独立的事务:
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 迁移
旧方式:
db, rdb := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, rdb)
store := postgres.NewXxxStore(db, rdb)
新方式:
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := postgres.NewXxxStore(tx, rdb)
故障排查
连接超时
如果测试跳过并显示"无法连接测试数据库":
- 检查网络连接
- 验证数据库服务状态
- 确认 DSN 配置正确
事务死锁
如果测试卡住:
- 检查是否有未提交的长事务
- 避免在单个测试中执行耗时操作
- 确保测试 < 1 秒完成
Redis 键冲突
如果出现数据污染:
- 确保使用
CleanTestRedisKeys - 检查是否正确使用
GetTestRedisKeyPrefix - 验证键名是否包含测试名称前缀