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

6.5 KiB
Raw Blame History

测试数据库连接管理规范

本文档是测试连接管理的唯一标准,所有新测试必须遵循此规范。

⚠️ 运行测试前必须加载环境变量

所有测试命令必须先加载 .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)

故障排查

连接超时

如果测试跳过并显示"无法连接测试数据库":

  1. 检查网络连接
  2. 验证数据库服务状态
  3. 确认 DSN 配置正确

事务死锁

如果测试卡住:

  1. 检查是否有未提交的长事务
  2. 避免在单个测试中执行耗时操作
  3. 确保测试 < 1 秒完成

Redis 键冲突

如果出现数据污染:

  1. 确保使用 CleanTestRedisKeys
  2. 检查是否正确使用 GetTestRedisKeyPrefix
  3. 验证键名是否包含测试名称前缀