- 新增 templates/go-integration-test.md 集成测试代码骨架模板 - SKILL.md 增加 TG2 集成测试检测:跨 handlers/middleware/routes 变更自动触发 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.6 KiB
Markdown
128 lines
4.6 KiB
Markdown
# Go 集成测试模板
|
||
|
||
## 适用场景
|
||
|
||
当 git diff 显示同一功能的 **handlers/ + middleware/ + routes/** 文件均有变更时,除单元测试外应额外生成集成测试。
|
||
|
||
适用于:
|
||
- 多步骤 API 流程(登录→操作→验证→退出)
|
||
- 中间件拦截验证(权限、限流、模拟状态限制)
|
||
- 租户隔离 / 数据隔离验证
|
||
- 跨模块交互(handler ↔ middleware ↔ repository)
|
||
|
||
## 检测规则
|
||
|
||
通过命名模式识别同一功能的跨文件变更:
|
||
|
||
```
|
||
handlers/{feature}_handler.go
|
||
middleware/{feature}_middleware.go
|
||
routes/{feature}_routes.go
|
||
```
|
||
|
||
例如 `impersonation_handler.go` + `impersonation_middleware.go` + `impersonation_routes.go` 同时变更 → 生成 `tests/impersonation_integration_test.go`。
|
||
|
||
## 生成规则
|
||
|
||
1. 测试文件放在 `backend/tests/` 目录(与 `test_helpers.go` 同包)
|
||
2. 使用 `SetupTestApp` + `defer TeardownTestApp` 初始化真实路由和数据库
|
||
3. 每个测试完全自包含:独立创建/清理数据
|
||
4. 异步持久化操作(goroutine 写 DB)需 `time.Sleep(200-500ms)` 后再验证
|
||
|
||
## 代码骨架
|
||
|
||
```go
|
||
package tests
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestFeature_Scenario(t *testing.T) {
|
||
testApp := SetupTestApp(t)
|
||
defer TeardownTestApp(t, testApp)
|
||
|
||
// === Setup: 创建测试数据 ===
|
||
// 使用 test_helpers.go 中的 helper 函数
|
||
sysAdmin := CreateTestSystemUser(t, testApp, "admin_scenario")
|
||
// tenantID := CreateTestTenant(t, testApp, "scenario_tenant")
|
||
// enterprise := CreateTestEnterpriseWithTenant(t, testApp, "Corp", "CODE", tenantID)
|
||
// tenantAdmin := CreateTestTenantAdmin(t, testApp, "tadmin", enterprise.ID, tenantID)
|
||
|
||
// === Cleanup: 按外键约束顺序清理 ===
|
||
defer CleanupImpersonationTestData(t, testApp,
|
||
[]int{sysAdmin.ID},
|
||
[]int{}, // enterpriseIDs
|
||
[]int64{}, // tenantIDs
|
||
)
|
||
|
||
// === 状态变量:跨步骤传递 ===
|
||
var token string
|
||
|
||
t.Run("Step1_InitialAction", func(t *testing.T) {
|
||
w := MakeAuthenticatedRequest(t, testApp, http.MethodPost,
|
||
"/api/v1/...",
|
||
map[string]string{"key": "value"},
|
||
sysAdmin,
|
||
)
|
||
require.Equal(t, http.StatusOK, w.Code, "Step1 failed: %s", w.Body.String())
|
||
|
||
var resp map[string]interface{}
|
||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||
// 提取后续步骤需要的数据
|
||
token = resp["data"].(map[string]interface{})["token"].(string)
|
||
})
|
||
|
||
t.Run("Step2_VerifyState", func(t *testing.T) {
|
||
if token == "" {
|
||
t.Skip("Step1 failed")
|
||
}
|
||
// 使用 Step1 产出的 token 继续验证
|
||
})
|
||
|
||
t.Run("StepN_AsyncVerification", func(t *testing.T) {
|
||
// 异步写入的数据需要等待
|
||
time.Sleep(300 * time.Millisecond)
|
||
// 然后验证 DB 数据
|
||
})
|
||
}
|
||
```
|
||
|
||
## 4 种必测场景
|
||
|
||
| 场景类型 | 说明 | 示例 |
|
||
|----------|------|------|
|
||
| **Happy Path** | 完整正常流程 | 开始模拟 → 访问 API → 查状态 → 退出 → 确认恢复 |
|
||
| **权限拒绝** | 无权限用户尝试操作 | 普通用户尝试模拟 → 403 |
|
||
| **隔离验证** | 跨租户/跨企业数据隔离 | tenant2 admin 访问 tenant1 企业 → 403 |
|
||
| **边界条件** | 输入校验、状态冲突 | 原因太短 → 400;已在模拟中再次模拟 → 403 |
|
||
|
||
## Helper 函数参考
|
||
|
||
```go
|
||
// 来自 test_helpers.go
|
||
SetupTestApp(t) // 初始化完整应用(路由 + DB)
|
||
CreateTestSystemUser(t, app, username) // 系统管理员 + JWT
|
||
CreateTestTenant(t, app, name) // 创建租户,返回 int64 ID
|
||
CreateTestEnterpriseWithTenant(t, app, name, code, tenantID) // 带租户的企业
|
||
CreateTestTenantAdmin(t, app, username, enterpriseID, tenantID...) // 租户管理员
|
||
CreateTestEnterpriseUser(t, app, username, enterpriseID) // 普通企业用户
|
||
ExtractImpersonationToken(t, response) // 从模拟响应提取 token
|
||
MakeAuthenticatedRequest(t, app, method, path, body, user) // 发送认证请求
|
||
CleanupImpersonationTestData(t, app, userIDs, enterpriseIDs, tenantIDs) // 按FK顺序清理
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
- **Token 链式传递**:模拟 API 返回新 token,后续请求必须用新 token
|
||
- **异步持久化**:handler 用 goroutine 写 session/audit,测试需 Sleep 后再查
|
||
- **数据隔离**:每个测试用唯一的 username/code,避免测试间干扰
|
||
- **清理顺序**:外键约束要求先删子表再删父表
|