Go语言单元测试与基准测试

Go语言在语言层面内置了测试支持,通过testing包提供单元测试、基准测试、示例测试等功能。编写测试代码无需引入外部框架,只需遵循简单的命名规则(*_test.go文件,TestXxx函数名)。这种设计使得测试编写变得轻量且高效,鼓励开发者养成测试驱动的习惯。

核心命令:go test 自动运行测试;go test -bench . 运行基准测试;go test -cover 查看测试覆盖率。

1. 第一个单元测试

假设我们有一个math.go文件,包含待测试的函数:

待测试代码 (math.go)
package main

func Add(a, b int) int {
    return a + b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

对应的测试文件math_test.go:

单元测试 (math_test.go)
package main

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2,3) = %d; want %d", result, expected)
    }
}

func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10,2) = %d; want 5", result)
    }

    // 测试除零错误
    _, err = Divide(5, 0)
    if err == nil {
        t.Error("expected error but got nil")
    }
}

运行测试:go test 或 go test -v(显示详细输出)。

2. 表驱动测试

表驱动测试是Go中常见的测试模式,通过定义测试用例切片,循环执行并判断结果,极大减少重复代码。

表驱动测试示例
func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 1, 2, 3},
        {"zero", 0, 0, 0},
        {"negative", -1, -2, -3},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

3. 子测试

使用t.Run可以创建子测试,每个子测试可以独立运行,失败时只影响自身。支持通过go test -run TestXxx/subtest单独运行子测试。

func TestDivide(t *testing.T) {
    t.Run("normal division", func(t *testing.T) {
        got, err := Divide(10, 2)
        if err != nil {
            t.Fatal(err)
        }
        if got != 5 {
            t.Errorf("got %d, want 5", got)
        }
    })
    t.Run("division by zero", func(t *testing.T) {
        _, err := Divide(5, 0)
        if err == nil {
            t.Error("expected error, got nil")
        }
    })
}

4. 测试辅助函数

在辅助函数中调用t.Helper()可以报告错误时定位到调用者的行号,而非辅助函数内部。

func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestAddWithHelper(t *testing.T) {
    assertEqual(t, Add(2,3), 5)
}

5. 跳过与并行测试

t.Skip可以跳过某些测试(如依赖外部环境)。t.Parallel可将测试标记为并行运行。

func TestDatabase(t *testing.T) {
    if os.Getenv("DB_TEST") == "" {
        t.Skip("跳过数据库测试,未设置 DB_TEST 环境变量")
    }
    // 数据库测试...
}

func TestParallel(t *testing.T) {
    t.Parallel() // 此测试将与其他并行测试同时运行
    // 测试逻辑
}

6. 基准测试

基准测试用于评估代码性能。函数名必须以Benchmark开头,参数类型为*testing.B。循环次数由b.N决定,测试框架会自动调整。

基准测试示例
// 待测试函数(字符串拼接)
func ConcatSimple(s []string) string {
    result := ""
    for _, v := range s {
        result += v
    }
    return result
}

func ConcatBuilder(s []string) string {
    var builder strings.Builder
    for _, v := range s {
        builder.WriteString(v)
    }
    return builder.String()
}

// 基准测试函数
func BenchmarkConcatSimple(b *testing.B) {
    strs := []string{"a", "b", "c", "d", "e"}
    for i := 0; i < b.N; i++ {
        ConcatSimple(strs)
    }
}

func BenchmarkConcatBuilder(b *testing.B) {
    strs := []string{"a", "b", "c", "d", "e"}
    for i := 0; i < b.N; i++ {
        ConcatBuilder(strs)
    }
}

运行基准测试:go test -bench=. 或 go test -bench=Concat。结果示例:

BenchmarkConcatSimple-8 5000000 320 ns/op
BenchmarkConcatBuilder-8 20000000 80.1 ns/op

7. 高级基准测试选项

  • -benchmem 显示内存分配统计(B/op, allocs/op)。
  • -benchtime=5s 延长测试时间。
  • -count=5 重复多次取平均值。
go test -bench=Concat -benchmem -cpu=1,2,4

8. 测试覆盖率

go test -cover 显示覆盖率百分比;go test -coverprofile=c.out 生成覆盖率文件;go tool cover -html=c.out 在浏览器中查看未覆盖的代码行。

9. 示例测试(Example)

示例测试用于文档中展示用法,并可验证输出是否与// Output:注释匹配。

func ExampleAdd() {
    sum := Add(3, 5)
    fmt.Println(sum)
    // Output: 8
}

综合示例:斐波那契函数测试

测试 + 基准测试
package main

// 递归版斐波那契
func FibRecursive(n int) int {
    if n < 2 {
        return n
    }
    return FibRecursive(n-1) + FibRecursive(n-2)
}

// 迭代版斐波那契
func FibIterative(n int) int {
    if n < 2 {
        return n
    }
    a, b := 0, 1
    for i := 2; i <= n; i++ {
        a, b = b, a+b
    }
    return b
}

// 测试函数
func TestFib(t *testing.T) {
    tests := []struct{ n, want int }{
        {0, 0}, {1, 1}, {2, 1}, {3, 2}, {10, 55},
    }
    for _, tt := range tests {
        got := FibIterative(tt.n)
        if got != tt.want {
            t.Errorf("Fib(%d) = %d, want %d", tt.n, got, tt.want)
        }
    }
}

// 基准测试对比
func BenchmarkFibRecursive(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibRecursive(20)
    }
}

func BenchmarkFibIterative(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibIterative(20)
    }
}

运行go test -bench=Fib -benchmem可看到迭代版性能远超递归版。

小结:Go内置的测试工具链让测试变得简单而高效。编写_test.go文件,使用go test即可快速验证正确性和性能。表驱动测试和子测试是推荐的最佳实践。基准测试则帮助我们发现性能瓶颈,指导优化方向。持续编写测试是构建可靠Go服务的关键。