测试指南
本文档提供井云服务中心的详细测试规范和指南,帮助开发者编写高质量的测试代码。
目录
测试概述
测试分层
项目采用以下测试分层策略:
┌─────────────────────────────────────┐
│ 端到端测试 (E2E Tests) │
│ (Playwright - 前端) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 集成测试 (Integration Tests) │
│ (服务间通信 + 数据库) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 单元测试 (Unit Tests) │
│ (业务逻辑 + Mock 依赖) │
└─────────────────────────────────────┘
测试工具
- 单元测试: testify + go-sqlmock
- 前端测试: Playwright
- 代码覆盖率: go test -cover
测试类型划分
测试类型定义
| 测试类型 | 目的 | 是否统计覆盖率 | 执行速度 | 依赖 |
|---|---|---|---|---|
| 单元测试 (UT) | 验证最小业务单元(函数/方法/业务规则) | ✅ 是(核心) | 毫秒级 | mock / fake |
| 集成测试 (IT) | 验证模块之间是否能"一起工作" | ❌ 不强制 | 秒级 | 真实 DB / Redis / fake API |
| 回归测试 (RT) | 防止历史问题复发(针对修过的 Bug) | ❌ 不强制 | 秒级 | 同 UT 或 IT |
| E2E / 系统测试 | 验证整体链路 | ❌ 不统计 | 分钟级 | 完整环境 |
核心原则
👉 覆盖率 = 单元测试指标
👉 集成/回归测试用于"兜底",不用于"刷覆盖率"
👉 回归测试本质上就是 UT 或 IT,只是语义上不可删除
单元测试 (UT) 标准
定义:
- 测试目标: 函数 / 方法 / 业务规则
- 依赖: mock / fake
- 不依赖: 真实 DB / 网络
- 执行速度: 毫秒级
覆盖率要求:
| 模块 | 最低要求 | 理想目标 |
|---|---|---|
| 整体项目 | ≥ 70% | 80%+ |
| biz 层 | ≥ 80% | 90%+ |
| adapter / gateway | ≥ 70% | 85%+ |
| service 层 | ≥ 60% | 75%+ |
| data 层 | ≥ 50% | 65%+ |
| AI / Agent 模块 | ≥ 85% | 95%+ |
| 协议 Adapter | ≥ 70% | 85%+ |
核心业务覆盖率: 必须达到 90%+
准入标准:
- ✅ 必须覆盖: 正常路径、错误路径、边界条件
- ❌ 禁止: 依赖真实第三方(Coze、Redis、MySQL)、使用 sleep / time.After
- ✅ 允许: mock、table-driven test
集成测试 (IT) 标准
定义:
- 测试目标: 模块之间是否能"一起工作"
- 依赖: 真实 DB / Redis(测试实例)、fake 外部 API
- 执行时间: 秒级
覆盖范围(不算覆盖率):
| 场景 | 是否必须 |
|---|---|
| HTTP / gRPC 完整请求 | ✅ |
| 中间件链路 | ✅ |
| 数据持久化 | ✅ |
| 事务 / 回滚 | ✅ |
| 配置加载 | ✅ |
CI 触发规则:
- PR: ❌ 可不跑
- main / release: ✅ 必须跑
- hotfix: ✅ 必须跑
回归测试 (RT) 标准
定义: 回归测试 = 针对"修过的 Bug"写的测试
什么时候必须写:
| 情况 | 是否必须 |
|---|---|
| 线上 Bug | ✅ |
| 数据错误 | ✅ |
| 权限漏洞 | ✅ |
| AI / Agent 输出异常 | ✅ |
测试规则:
- 测试名必须能看出 bug 语义:
Test<ServiceName>_<MethodName>_Regression_<BugDescription> - 必须先失败(复现 bug),修复后通过
- 禁止合并后删除
- 禁止只测 happy path
三种测试在 CI 中的职责分工
| 阶段 | UT | IT | RT |
|---|---|---|---|
| PR | ✅ 必须 | ❌ 可选 | ✅(若涉及 bug) |
| main | ✅ 必须 | ✅ 必须 | ✅ 必须 |
| release | ✅ 必须 | ✅ 必须 | ✅ 必须 |
| hotfix | ✅ 必须 | ✅ 必须 | ✅ 必须 |
常见误区
❌ 误区 1: 集成测试也算覆盖率
✅ 正确: 覆盖率只看 UT
❌ 误区 2: 为了覆盖率写 IT
✅ 正确: IT 用于验证模块协作,UT 用于验证业务逻辑
❌ 误区 3: 回归测试单独一套系统
✅ 正确: 回归测试就是有特殊命名的 UT 或 IT,不可删除
测试命名规范
测试文件命名
测试文件必须与源文件对应,添加_test.go后缀:
源文件: agent.go
测试文件: agent_test.go
测试用例命名
测试用例名称必须遵循以下格式:
Test<ServiceName>_<MethodName>_<Scenario>
示例:
// 成功场景
func TestAgentService_CreateAgent_Success(t *testing.T) {}
// 错误场景
func TestAgentService_CreateAgent_NoTenantID(t *testing.T) {}
func TestAgentService_CreateAgent_DatabaseError(t *testing.T) {}
// 边界场景
func TestAgentService_CreateAgent_EmptyName(t *testing.T) {}
func TestAgentService_CreateAgent_NameTooLong(t *testing.T) {}
Mock策略
Gateway服务 - 函数字段Mock
type MockAgentClient struct {
CreateAgentFunc func(ctx context.Context, in *agentv1.CreateAgentRequest, opts ...grpc.CallOption) (*agentv1.CreateAgentReply, error)
}
// 在测试中使用
mockClient := &MockAgentClient{
CreateAgentFunc: func(ctx context.Context, in *agentv1.CreateAgentRequest, opts ...grpc.CallOption) (*agentv1.CreateAgentReply, error) {
// 验证传入参数
require.Equal(t, int64(1), in.TenantId)
// 返回Mock数据
return &agentv1.CreateAgentReply{Id: 1}, nil
},
}
数据库服务 - sqlmock + Ent
func setupServiceTest(t *testing.T) (*sql.DB, sqlmock.Sqlmock, *AgentService) {
// 1. 创建 sqlmock
mockDB, mock, err := sqlmock.New()
require.NoError(t, err)
// 2. 使用 Ent driver 包装 mock DB
drv := entsql.OpenDB("postgres", mockDB)
entClient := entclient.NewClient(entclient.Driver(drv))
// 3. 初始化服务
svc := NewAgentService(entClient, logger)
return mockDB, mock, svc
}
Mock SQL INSERT:
rows := sqlmock.NewRows([]string{"id"}).AddRow(int64(1))
mock.ExpectQuery(`INSERT INTO "agents"`).WillReturnRows(rows)
Mock SQL SELECT:
rows := sqlmock.NewRows([]string{"id", "name", "type"}).
AddRow(int64(1), "测试智能体", "bot")
mock.ExpectQuery(`SELECT .* FROM "agents"`).WillReturnRows(rows)
测试场景覆盖
必须包含的测试场景
每个接口必须至少包含以下测试场景:
- 成功场景: 正常业务流程
- 错误场景: 异常情况
- 边界场景: 边界条件
成功场景示例
func TestAgentService_CreateAgent_Success(t *testing.T) {
mockDB, mock, svc := setupServiceTest(t)
defer mockDB.Close()
req := &agentv1.CreateAgentRequest{
TenantId: 1,
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
}
rows := sqlmock.NewRows([]string{"id"}).AddRow(int64(1))
mock.ExpectQuery(`INSERT INTO "agents"`).WillReturnRows(rows)
resp, err := svc.CreateAgent(context.Background(), req)
require.NoError(t, err)
require.Equal(t, int64(1), resp.Id)
require.NoError(t, mock.ExpectationsWereMet())
}
错误场景示例
func TestAgentService_CreateAgent_NoTenantID(t *testing.T) {
mockDB, mock, svc := setupServiceTest(t)
defer mockDB.Close()
req := &agentv1.CreateAgentRequest{
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
}
resp, err := svc.CreateAgent(context.Background(), req)
require.Error(t, err)
require.Nil(t, resp)
}
测试断言规范
require断言
关键断言,失败时立即停止测试:
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, int64(1), resp.Id)
assert断言
普通断言,失败后继续执行:
assert.Equal(t, int64(1), resp.Agent.Id)
assert.Equal(t, "测试智能体", resp.Agent.Name)
assert.True(t, resp.Agent.IsActive)
完整字段验证
原则: 返回的结构体所有字段都必须验证
// ✅ 正确 - 验证所有字段
assert.Equal(t, int64(1), resp.Agent.Id)
assert.Equal(t, int64(1), resp.Agent.TenantId)
assert.Equal(t, "测试智能体", resp.Agent.Name)
assert.Equal(t, agentv1.AgentTypeBOT, resp.Agent.Type)
assert.True(t, resp.Agent.IsActive)
assert.Equal(t, int64(1672531200), resp.Agent.CreatedAt)
assert.Equal(t, int64(1672531200), resp.Agent.UpdatedAt)
// ❌ 错误 - 字段验证不完整
assert.Equal(t, int64(1), resp.Agent.Id)
assert.Equal(t, "测试智能体", resp.Agent.Name)
测试执行
运行单个服务测试
cd services/agent
go test ./internal/service/... -v
运行单个测试用例
go test ./internal/service/... -run TestCreateAgent_Success -v
查看覆盖率
go test ./internal/service/... -cover
生成覆盖率报告
go test ./internal/service/... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
批量运行所有测试
cd backend
make test-coverage-summary # 生成测试覆盖率汇总
make test-coverage-html # 生成 HTML 覆盖率报告
make test-coverage-report # 生成综合覆盖率报告