Files
junhong_cmp_fiber/tests/testutils/db.go
huang b68e7ec013
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
2026-01-22 14:38:43 +08:00

224 lines
5.6 KiB
Go
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.
package testutils
import (
"context"
"fmt"
"sync"
"testing"
"github.com/redis/go-redis/v9"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// 全局单例数据库和 Redis 连接
// 使用 sync.Once 确保整个测试套件只创建一次连接,显著提升测试性能
var (
testDBOnce sync.Once
testDB *gorm.DB
testDBInitErr error
testRedisOnce sync.Once
testRedis *redis.Client
testRedisInitErr error
)
// 测试数据库配置
// TODO: 未来可以从环境变量或配置文件加载
const (
testDBDSN = "host=cxd.whcxd.cn port=16159 user=erp_pgsql password=erp_2025 dbname=junhong_cmp_test sslmode=disable TimeZone=Asia/Shanghai"
testRedisAddr = "cxd.whcxd.cn:16299"
testRedisPasswd = "cpNbWtAaqgo1YJmbMp3h"
testRedisDB = 15
)
// GetTestDB 获取全局单例测试数据库连接
//
// 特点:
// - 使用 sync.Once 确保整个测试套件只创建一次连接
// - AutoMigrate 只在首次连接时执行一次
// - 连接失败会跳过测试(不是致命错误)
//
// 用法:
//
// func TestXxx(t *testing.T) {
// db := testutils.GetTestDB(t)
// // db 是全局共享的连接,不要直接修改其状态
// // 如需事务隔离,使用 NewTestTransaction(t)
// }
func GetTestDB(t *testing.T) *gorm.DB {
t.Helper()
testDBOnce.Do(func() {
var err error
testDB, err = gorm.Open(postgres.Open(testDBDSN), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
testDBInitErr = fmt.Errorf("无法连接测试数据库: %w", err)
return
}
// AutoMigrate 只执行一次(幂等操作,但耗时约 100ms
err = testDB.AutoMigrate(
&model.Account{},
&model.Role{},
&model.Permission{},
&model.AccountRole{},
&model.RolePermission{},
&model.Shop{},
&model.Enterprise{},
&model.PersonalCustomer{},
)
if err != nil {
testDBInitErr = fmt.Errorf("数据库迁移失败: %w", err)
return
}
})
if testDBInitErr != nil {
t.Skipf("跳过测试:%v", testDBInitErr)
}
return testDB
}
// GetTestRedis 获取全局单例 Redis 连接
//
// 特点:
// - 使用 sync.Once 确保整个测试套件只创建一次连接
// - 连接失败会跳过测试(不是致命错误)
//
// 用法:
//
// func TestXxx(t *testing.T) {
// rdb := testutils.GetTestRedis(t)
// // rdb 是全局共享的连接
// // 使用 CleanTestRedisKeys(t) 自动清理测试相关的 Redis 键
// }
func GetTestRedis(t *testing.T) *redis.Client {
t.Helper()
testRedisOnce.Do(func() {
testRedis = redis.NewClient(&redis.Options{
Addr: testRedisAddr,
Password: testRedisPasswd,
DB: testRedisDB,
})
ctx := context.Background()
if err := testRedis.Ping(ctx).Err(); err != nil {
testRedisInitErr = fmt.Errorf("无法连接 Redis: %w", err)
return
}
})
if testRedisInitErr != nil {
t.Skipf("跳过测试:%v", testRedisInitErr)
}
return testRedis
}
// NewTestTransaction 创建测试事务,自动在测试结束时回滚
//
// 特点:
// - 每个测试用例获得独立的事务,互不干扰
// - 使用 t.Cleanup() 确保即使测试 panic 也能回滚
// - 回滚后数据库状态与测试前完全一致
//
// 用法:
//
// func TestXxx(t *testing.T) {
// tx := testutils.NewTestTransaction(t)
// // 所有数据库操作使用 tx 而非 db
// store := postgres.NewXxxStore(tx, rdb)
// // 测试结束后自动回滚,无需手动清理
// }
//
// 注意:
// - 不要在子测试(t.Run)中调用此函数,因为子测试可能并行执行
// - 如需在子测试中使用数据库,应在父测试中创建事务并传递
func NewTestTransaction(t *testing.T) *gorm.DB {
t.Helper()
db := GetTestDB(t)
tx := db.Begin()
if tx.Error != nil {
t.Fatalf("开启测试事务失败: %v", tx.Error)
}
// 使用 t.Cleanup() 确保测试结束时自动回滚
// 即使测试 panic 也能执行清理
t.Cleanup(func() {
tx.Rollback()
})
return tx
}
// CleanTestRedisKeys 清理当前测试的 Redis 键
//
// 特点:
// - 使用测试名称作为键前缀,格式: test:{TestName}:*
// - 测试开始时清理已有键(防止脏数据)
// - 使用 t.Cleanup() 确保测试结束时自动清理
//
// 用法:
//
// func TestXxx(t *testing.T) {
// rdb := testutils.GetTestRedis(t)
// testutils.CleanTestRedisKeys(t, rdb)
// // Redis 键使用测试专用前缀: test:TestXxx:your_key
// }
//
// 键命名规范:
// - 测试中创建的键应使用 GetTestRedisKeyPrefix(t) 作为前缀
// - 例如: test:TestShopStore_Create:cache:shop:1
func CleanTestRedisKeys(t *testing.T, rdb *redis.Client) {
t.Helper()
ctx := context.Background()
testPrefix := GetTestRedisKeyPrefix(t)
// 测试开始前清理已有键
cleanKeys(ctx, rdb, testPrefix)
// 测试结束时自动清理
t.Cleanup(func() {
cleanKeys(ctx, rdb, testPrefix)
})
}
// GetTestRedisKeyPrefix 获取当前测试的 Redis 键前缀
//
// 返回格式: test:{TestName}:
// 用于在测试中创建带前缀的 Redis 键,确保键不会与其他测试冲突
//
// 用法:
//
// func TestXxx(t *testing.T) {
// prefix := testutils.GetTestRedisKeyPrefix(t)
// key := prefix + "my_cache_key"
// // key = "test:TestXxx:my_cache_key"
// }
func GetTestRedisKeyPrefix(t *testing.T) string {
t.Helper()
return fmt.Sprintf("test:%s:", t.Name())
}
// cleanKeys 清理匹配前缀的所有 Redis 键
func cleanKeys(ctx context.Context, rdb *redis.Client, prefix string) {
keys, err := rdb.Keys(ctx, prefix+"*").Result()
if err != nil {
// 忽略 Redis 错误,不影响测试
return
}
if len(keys) > 0 {
rdb.Del(ctx, keys...)
}
}