跳到主要内容

测试指南

本文档提供井云服务中心的详细测试规范和指南,帮助开发者编写高质量的测试代码。

目录

测试概述

测试分层

项目采用以下测试分层策略:

┌─────────────────────────────────────┐
│ 端到端测试 (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 中的职责分工

阶段UTITRT
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)

测试场景覆盖

必须包含的测试场景

每个接口必须至少包含以下测试场景:

  1. 成功场景: 正常业务流程
  2. 错误场景: 异常情况
  3. 边界场景: 边界条件

成功场景示例

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 # 生成综合覆盖率报告

测试最佳实践

1. 每个测试用例只测试一个功能点

// ✅ 正确
func TestCreateAgent_Success(t *testing.T) {
// 只测试创建成功的场景
}

// ❌ 错误
func TestCreateAgent_SuccessAndUpdate(t *testing.T) {
// 测试了创建和更新两个功能点
}

2. 测试用例之间相互独立

// ✅ 正确
func TestCreateAgent_Success(t *testing.T) {
mockDB, mock, svc := setupServiceTest(t)
defer mockDB.Close()
// 独立测试
}

// ❌ 错误
var globalSvc *AgentService

func TestCreateAgent_Success(t *testing.T) {
globalSvc = setupServiceTest(t)
// 依赖全局状态
}

3. 使用有意义的测试名称

// ✅ 正确
func TestAgentService_CreateAgent_Success(t *testing.T) {}
func TestAgentService_CreateAgent_NoTenantID(t *testing.T) {}

// ❌ 错误
func TestAgentService_CreateAgent_1(t *testing.T) {}
func TestAgentService_CreateAgent_2(t *testing.T) {}

4. 测试数据使用工厂方法

func createTestAgent(id int64, tenantId int64) *agentv1.Agent {
return &agentv1.Agent{
Id: id,
TenantId: tenantId,
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
IsActive: true,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
}

参考资料


附录:统一测试规范摘要

核心原则

  1. 单元测试是覆盖率唯一统计来源
  2. 覆盖率不足禁止合并
  3. 集成测试不统计覆盖率,但 main 分支必须通过
  4. 所有线上 Bug 必须补回归测试
  5. 回归测试不得删除
  6. AI / Agent 模块测试标准高于普通业务

测试类型快速参考

测试类型目的覆盖率CI 触发
单元测试验证业务逻辑✅ 统计PR 必须
集成测试验证模块协作❌ 不统计main 必须
回归测试防止 Bug 复发❌ 不统计Bug 修复必须

AI / Agent 场景补充要求

模块要求
Agent 决策逻辑UT ≥ 85%
协议 AdapterUT ≥ 70%
流式 SSE必须有 IT
Prompt / Tool Router必须有回归测试

7.1 Agent 决策逻辑测试示例

func TestAgentDecision_ChooseTool_Success(t *testing.T) {
// 测试 Agent 根据用户输入选择正确的工具
decision := NewAgentDecision()

tool, err := decision.ChooseTool("查询天气")
require.NoError(t, err)
require.Equal(t, "weather_tool", tool.Name)

tool, err = decision.ChooseTool("计算数学")
require.NoError(t, err)
require.Equal(t, "calculator_tool", tool.Name)
}

7.2 流式 SSE 集成测试示例

//go:build integration

func TestAgent_StreamSSE_Integration(t *testing.T) {
// 使用真实连接测试流式响应
svc := NewAgentService(realDB, logger)

// 创建 SSE 客户端
client := NewSSEClient()

// 发起流式请求
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

events, err := client.Stream(ctx, &agentv1.StreamRequest{
AgentId: 1,
Message: "测试流式响应",
})
require.NoError(t, err)

// 验证流式事件
count := 0
for event := range events {
require.NotNil(t, event)
count++
}
require.Greater(t, count, 0)
}

覆盖率准入标准

  • ✅ 整体项目 ≥ 70%
  • ✅ biz 层 ≥ 80%
  • ✅ AI / Agent 模块 ≥ 85%
  • ✅ 核心业务覆盖率 ≥ 90%
  • ❌ 不满足禁止合并

CI 检查清单

  • 单元测试全部通过
  • 覆盖率达标
  • 线上 Bug 已补回归测试
  • 回归测试通过
  • main 分支集成测试通过

最后更新: 2025-12-29