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
224 lines
5.6 KiB
Go
224 lines
5.6 KiB
Go
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...)
|
||
}
|
||
}
|