测试与代码质量¶
学习目标¶
学完本章后,学习者应该能够:
- 编写单元测试、表格驱动测试和 benchmark。
- 理解 Mock、race detector、测试覆盖率和代码审查。
- 掌握 Go 命名、包设计、小接口和代码风格。
- 能为后端服务建立基础质量门禁。
表格驱动测试¶
Go 社区常用表格驱动测试:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{name: "valid", email: "a@example.com", wantErr: false},
{name: "missing at", email: "invalid", wantErr: true},
{name: "empty", email: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
它适合覆盖正常、异常、边界、空输入和重复输入。
Benchmark¶
Benchmark 用于比较性能:
func BenchmarkBuildNames(b *testing.B) {
users := make([]User, 1000)
for i := 0; i < b.N; i++ {
_ = BuildNames(users)
}
}
运行:
Benchmark 结果要结合可读性和维护成本,不要为微小收益写复杂代码。
Mock 与测试边界¶
单元测试应该尽量隔离外部依赖。可以通过接口注入 fake:
type fakeUserRepo struct {
user User
err error
}
func (f fakeUserRepo) Get(ctx context.Context, id int64) (User, error) {
return f.user, f.err
}
不是所有测试都要 mock。数据库、Redis、消息队列等关键集成也需要集成测试。
race detector¶
并发代码必须跑 race detector:
它能发现测试覆盖路径中的数据竞争。
代码质量习惯¶
好代码通常具备:
- 命名表达业务含义。
- 函数短而聚焦。
- 错误有上下文。
- 包依赖方向清晰。
- 并发有退出路径。
- 外部调用有超时。
- 测试覆盖关键边界。
代码审查不只是挑格式,而是发现边界、风险和可维护性问题。
Go 后端实际应用例子¶
例子一:Service 单元测试¶
func TestAPIKeyService_CreateDuplicate(t *testing.T) {
repo := fakeAPIKeyRepo{findErr: nil}
svc := NewAPIKeyService(repo)
err := svc.Create(context.Background(), CreateAPIKeyInput{Name: "default"})
if !errors.Is(err, ErrAPIKeyExists) {
t.Fatalf("err = %v, want %v", err, ErrAPIKeyExists)
}
}
service 测试重点验证业务规则,不应该依赖真实数据库。
例子二:CI 基础命令¶
这些命令是最基础的质量门禁。
常见误区¶
- 误区一:测试覆盖率越高质量越好。
覆盖率只是指标。关键路径、边界条件、错误路径和并发风险更重要。
- 误区二:只测 happy path。
后端服务最容易出问题的是异常、超时、重复请求、空输入和依赖失败。
- 误区三:Code Review 只是看语法。
Review 应关注行为、边界、错误处理、可观测性、安全和可维护性。
实战任务¶
为用户注册校验函数编写测试:
- 覆盖合法输入。
- 覆盖空邮箱。
- 覆盖密码过短。
- 覆盖年龄不足。
- 使用表格驱动测试。
参考答案
示例测试:
func TestRegisterRequest_Validate(t *testing.T) {
tests := []struct {
name string
req RegisterRequest
wantErr bool
}{
{name: "valid", req: RegisterRequest{Email: "a@example.com", Password: "12345678", Age: 18}},
{name: "empty email", req: RegisterRequest{Password: "12345678", Age: 18}, wantErr: true},
{name: "short password", req: RegisterRequest{Email: "a@example.com", Password: "123", Age: 18}, wantErr: true},
{name: "too young", req: RegisterRequest{Email: "a@example.com", Password: "12345678", Age: 17}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.req.Validate()
if (err != nil) != tt.wantErr {
t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
真实项目中还可以检查错误码,而不只是 wantErr。
面试题¶
1. 表格驱动测试有什么好处?¶
参考答案
表格驱动测试能把多组输入、输出和边界条件集中表达,结构清晰,方便新增用例。它非常适合纯函数、校验逻辑、算法和业务规则测试。
配合 t.Run 可以让每个用例有独立名称,失败时定位更快。
2. benchmark 应该怎么解读?¶
参考答案
benchmark 用于观察耗时和内存分配,-benchmem 可以显示分配次数和字节数。结果要多次运行,并结合实际输入规模、可读性和维护成本判断。
不要为了微小 benchmark 优势牺牲代码清晰度,除非它位于明确热路径。
3. 单元测试和集成测试有什么区别?¶
参考答案
单元测试关注小范围代码行为,通常隔离数据库、网络等外部依赖,速度快、定位准。集成测试关注多个组件真实协作,例如数据库 SQL、Redis、HTTP API 和消息队列。
后端项目两者都需要。单元测试保证业务规则,集成测试保证关键基础设施交互正确。