# 测试数据库连接管理规范

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

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

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

## 集成测试环境

对于需要完整 HTTP 请求测试的场景，使用 `IntegrationTestEnv`：

### 基础用法

```go
func TestAPI_Create(t *testing.T) {
    env := testutils.NewIntegrationTestEnv(t)

    t.Run("成功创建资源", func(t *testing.T) {
        reqBody := dto.CreateRequest{
            Name: fmt.Sprintf("test_%d", time.Now().UnixNano()),
        }

        jsonBody, _ := json.Marshal(reqBody)
        resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/resources", jsonBody)
        require.NoError(t, err)
        assert.Equal(t, fiber.StatusOK, resp.StatusCode)
    })
}
```

### IntegrationTestEnv API

| 方法 | 说明 |
|------|------|
| `NewIntegrationTestEnv(t)` | 创建集成测试环境，自动初始化所有依赖 |
| `AsSuperAdmin()` | 以超级管理员身份发送请求 |
| `AsUser(account)` | 以指定账号身份发送请求 |
| `Request(method, path, body)` | 发送 HTTP 请求 |
| `CreateTestAccount(...)` | 创建测试账号 |
| `CreateTestShop(...)` | 创建测试店铺 |
| `CreateTestRole(...)` | 创建测试角色 |
| `CreateTestPermission(...)` | 创建测试权限 |

### 数据隔离最佳实践

**必须使用动态生成的测试数据**，避免固定值导致的测试冲突：

```go
t.Run("创建资源", func(t *testing.T) {
    // ✅ 正确：使用动态值
    name := fmt.Sprintf("test_resource_%d", time.Now().UnixNano())
    phone := fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000)

    // ❌ 错误：使用固定值（会导致并发测试冲突）
    name := "test_resource"
    phone := "13800000001"
})
```

### 完整示例

```go
func TestAccountAPI_Create(t *testing.T) {
    env := testutils.NewIntegrationTestEnv(t)

    t.Run("成功创建平台账号", func(t *testing.T) {
        username := fmt.Sprintf("platform_user_%d", time.Now().UnixNano())
        phone := fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000)

        reqBody := dto.CreateAccountRequest{
            Username: username,
            Phone:    phone,
            Password: "Password123",
            UserType: constants.UserTypePlatform,
        }

        jsonBody, _ := json.Marshal(reqBody)
        resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
        require.NoError(t, err)
        assert.Equal(t, fiber.StatusOK, resp.StatusCode)

        // 验证数据库中账号已创建
        var count int64
        env.TX.Model(&model.Account{}).Where("username = ?", username).Count(&count)
        assert.Equal(t, int64(1), count)
    })
}
```

## 故障排查

### 连接超时

如果测试跳过并显示"无法连接测试数据库":
1. 检查网络连接
2. 验证数据库服务状态
3. 确认 DSN 配置正确

### 事务死锁

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

### Redis 键冲突

如果出现数据污染:
1. 确保使用 `CleanTestRedisKeys`
2. 检查是否正确使用 `GetTestRedisKeyPrefix`
3. 验证键名是否包含测试名称前缀

### 测试数据冲突

如果看到 "用户名已存在" 或 "手机号已存在" 错误:
1. 确保使用 `time.Now().UnixNano()` 生成唯一值
2. 不要在子测试之间共享固定的测试数据
3. 检查是否有遗留的测试数据未被事务回滚
