跳转至

测试与代码质量

学习目标

学完本章后,学习者应该能够:

  1. 编写单元测试、表格驱动测试和 benchmark。
  2. 理解 Mock、race detector、测试覆盖率和代码审查。
  3. 掌握 Go 命名、包设计、小接口和代码风格。
  4. 能为后端服务建立基础质量门禁。

表格驱动测试

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)
    }
}

运行:

go test -bench=. -benchmem ./...
go test -bench=. -benchmem ./...

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 test -race ./...
go test -race ./...

它能发现测试覆盖路径中的数据竞争。

代码质量习惯

好代码通常具备:

  • 命名表达业务含义。
  • 函数短而聚焦。
  • 错误有上下文。
  • 包依赖方向清晰。
  • 并发有退出路径。
  • 外部调用有超时。
  • 测试覆盖关键边界。

代码审查不只是挑格式,而是发现边界、风险和可维护性问题。

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 基础命令

go fmt ./...
go vet ./...
go test -race ./...
go fmt ./...
go vet ./...
go test -race ./...

这些命令是最基础的质量门禁。

常见误区

  • 误区一:测试覆盖率越高质量越好。

覆盖率只是指标。关键路径、边界条件、错误路径和并发风险更重要。

  • 误区二:只测 happy path。

后端服务最容易出问题的是异常、超时、重复请求、空输入和依赖失败。

  • 误区三:Code Review 只是看语法。

Review 应关注行为、边界、错误处理、可观测性、安全和可维护性。

实战任务

为用户注册校验函数编写测试:

  1. 覆盖合法输入。
  2. 覆盖空邮箱。
  3. 覆盖密码过短。
  4. 覆盖年龄不足。
  5. 使用表格驱动测试。
参考答案

示例测试:

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 和消息队列。

后端项目两者都需要。单元测试保证业务规则,集成测试保证关键基础设施交互正确。