测试最佳实践
本文档提供了测试编写的最佳实践,帮助开发者编写高质量、可维护的测试代码。
测试编写流程
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)
// ... 测试代码
}