跳到主要内容

测试最佳实践

本文档提供了测试编写的最佳实践,帮助开发者编写高质量、可维护的测试代码。

测试编写流程

1. 需求分析

在编写测试之前,先分析需求:

// 示例:创建智能体的需求分析
/*
需求:创建智能体
输入:TenantId, Name, Type
输出:Agent对象,包含ID
边界条件:
- TenantId不能为空
- Name不能为空
- Type必须是有效值
错误场景:
- 数据库连接失败
- 数据验证失败
- 权限不足
*/

2. 测试场景设计

根据需求设计测试场景:

// 成功场景
func TestCreateAgent_Success(t *testing.T) {}

// 错误场景
func TestCreateAgent_NoTenantID(t *testing.T) {}
func TestCreateAgent_EmptyName(t *testing.T) {}
func TestCreateAgent_InvalidType(t *testing.T) {}
func TestCreateAgent_DatabaseError(t *testing.T) {}

// 边界场景
func TestCreateAgent_NameTooLong(t *testing.T) {}
func TestCreateAgent_NameTooShort(t *testing.T) {}

3. 编写测试代码

按照测试规范编写测试代码:

func TestCreateAgent_Success(t *testing.T) {
// 1. 设置测试环境
mockDB, mock, svc := setupServiceTest(t)
defer mockDB.Close()

// 2. 准备测试数据
req := &agentv1.CreateAgentRequest{
TenantId: 1,
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
}

// 3. Mock外部依赖
rows := sqlmock.NewRows([]string{"id"}).AddRow(int64(1))
mock.ExpectQuery(`INSERT INTO "agents"`).WillReturnRows(rows)

// 4. 执行测试
resp, err := svc.CreateAgent(context.Background(), req)

// 5. 验证结果
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, int64(1), resp.Id)
require.NoError(t, mock.ExpectationsWereMet())
}

Table-Driven测试

基本用法

func TestCreateAgent(t *testing.T) {
tests := []struct {
name string
input *agentv1.CreateAgentRequest
want int64
wantErr bool
errMsg string
}{
{
name: "成功创建",
input: &agentv1.CreateAgentRequest{
TenantId: 1,
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
},
want: 1,
wantErr: false,
},
{
name: "缺少TenantId",
input: &agentv1.CreateAgentRequest{
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
},
want: 0,
wantErr: true,
errMsg: "tenant_id is required",
},
{
name: "名称为空",
input: &agentv1.CreateAgentRequest{
TenantId: 1,
Name: "",
Type: agentv1.AgentTypeBOT,
},
want: 0,
wantErr: true,
errMsg: "name is required",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDB, mock, svc := setupServiceTest(t)
defer mockDB.Close()

if !tt.wantErr {
rows := sqlmock.NewRows([]string{"id"}).AddRow(tt.want)
mock.ExpectQuery(`INSERT INTO "agents"`).WillReturnRows(rows)
} else {
mock.ExpectQuery(`INSERT INTO "agents"`).
WillReturnError(errors.New(tt.errMsg))
}

resp, err := svc.CreateAgent(context.Background(), tt.input)

if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, resp.Id)
}

require.NoError(t, mock.ExpectationsWereMet())
})
}
}

优点

  • 减少重复代码
  • 清晰展示测试场景
  • 易于添加新测试用例
  • 提高测试可读性
  • 便于维护

测试辅助函数

Setup函数

func setupServiceTest(t *testing.T) (*sql.DB, sqlmock.Sqlmock, *AgentService) {
// 创建sqlmock
mockDB, mock, err := sqlmock.New()
require.NoError(t, err)

// 使用Ent driver包装mock DB
drv := entsql.OpenDB("postgres", mockDB)
entClient := entclient.NewClient(entclient.Driver(drv))

// 初始化服务
logger := kmlog.NewStdLogger(io.Discard)
data := dataagent.NewData(mockDB, entClient, logger)
repo := dataagent.NewAgentRepo(data)
uc := bizagent.NewAgentUsecase(repo, logger)
svc := NewAgentService(uc, logger)

return mockDB, mock, svc
}

数据工厂函数

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

for _, opt := range opts {
opt(a)
}

return a
}

// 使用示例
agent1 := createTestAgent(1, 1)
agent2 := createTestAgent(2, 1, func(a *agentv1.Agent) {
a.Name = "自定义名称"
a.Type = agentv1.AgentTypeWORKFLOW
a.IsActive = false
})

断言辅助函数

func assertAgentEqual(t *testing.T, expected, actual *agentv1.Agent) {
t.Helper()

assert.Equal(t, expected.Id, actual.Id)
assert.Equal(t, expected.TenantId, actual.TenantId)
assert.Equal(t, expected.Name, actual.Name)
assert.Equal(t, expected.Type, actual.Type)
assert.Equal(t, expected.IsActive, actual.IsActive)
assert.Equal(t, expected.CreatedAt, actual.CreatedAt)
assert.Equal(t, expected.UpdatedAt, actual.UpdatedAt)
}

// 使用示例
assertAgentEqual(t, expectedAgent, actualAgent)

测试最佳实践清单

✅ 必须遵循

  • 每个测试用例只测试一个功能点
  • 测试用例之间相互独立,无依赖关系
  • 使用有意义的测试名称,描述测试场景
  • 测试数据使用工厂方法创建
  • 测试断言完整,验证所有返回字段
  • 使用Mock隔离外部依赖
  • 测试执行后清理测试数据
  • 验证所有Mock期望

✅ 推荐遵循

  • 使用Table-Driven测试减少重复代码
  • 使用测试辅助函数简化测试代码
  • 使用子测试组织相关测试
  • 使用测试套件组织测试
  • 使用测试覆盖率工具监控覆盖率
  • 使用基准测试优化性能
  • 使用模糊测试发现边界问题
  • 使用并发测试提高测试速度

❌ 避免这样做

  • 不要在测试中使用全局状态
  • 不要在测试中依赖执行顺序
  • 不要在测试中硬编码时间戳
  • 不要在测试中使用真实数据库
  • 不要在测试中使用真实外部服务
  • 不要在测试中忽略错误
  • 不要在测试中跳过断言
  • 不要在测试中写过多的注释

测试性能优化

并行测试

func TestAgentService_CreateAgent(t *testing.T) {
t.Parallel() // 启用并行测试

// 测试代码
}

跳过慢速测试

func TestAgentService_Integration(t *testing.T) {
if testing.Short() {
t.Skip("跳过集成测试")
}

// 测试代码
}

使用测试缓存

# 启用测试缓存
go test ./... -count=1

# 清除测试缓存
go clean -testcache

测试调试技巧

使用详细输出

# 使用-v参数查看详细输出
go test ./internal/service/... -v

# 使用-run参数运行特定测试
go test ./internal/service/... -run TestCreateAgent -v

使用调试模式

func TestAgentService_CreateAgent_Debug(t *testing.T) {
// 使用t.Log输出调试信息
t.Log("开始测试...")

mockDB, mock, svc := setupServiceTest(t)
defer mockDB.Close()

req := &agentv1.CreateAgentRequest{
TenantId: 1,
Name: "测试智能体",
Type: agentv1.AgentTypeBOT,
}

t.Logf("请求参数: %+v", req)

// ... 测试代码
}

使用测试覆盖率

# 生成覆盖率报告
go test ./internal/service/... -coverprofile=coverage.out

# 查看覆盖率详情
go tool cover -func=coverage.out

# 生成HTML报告
go tool cover -html=coverage.out -o coverage.html

参考资料


最后更新: 2025-12-29