1.7.2 测试覆盖率与基准测试

1.7.2 测试覆盖率与基准测试 #

测试覆盖率是衡量测试质量的重要指标,而基准测试则帮助我们了解代码的性能特征。本节将深入介绍如何使用 Go 的内置工具来分析测试覆盖率和编写性能基准测试。

测试覆盖率 #

测试覆盖率表示测试执行时覆盖的代码比例,是评估测试完整性的重要指标。Go 提供了强大的覆盖率分析工具。

基本覆盖率分析 #

让我们先创建一个示例程序来演示覆盖率分析:

// calculator.go
package main

import (
    "errors"
    "math"
)

type Calculator struct{}

// Add 加法运算
func (c *Calculator) Add(a, b float64) float64 {
    return a + b
}

// Subtract 减法运算
func (c *Calculator) Subtract(a, b float64) float64 {
    return a - b
}

// Multiply 乘法运算
func (c *Calculator) Multiply(a, b float64) float64 {
    return a * b
}

// Divide 除法运算
func (c *Calculator) Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Power 幂运算
func (c *Calculator) Power(base, exponent float64) float64 {
    return math.Pow(base, exponent)
}

// Sqrt 平方根运算
func (c *Calculator) Sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, errors.New("cannot calculate square root of negative number")
    }
    return math.Sqrt(x), nil
}

// IsEven 判断是否为偶数
func (c *Calculator) IsEven(n int) bool {
    return n%2 == 0
}

// Factorial 计算阶乘
func (c *Calculator) Factorial(n int) (int, error) {
    if n < 0 {
        return 0, errors.New("factorial of negative number is undefined")
    }
    if n == 0 || n == 1 {
        return 1, nil
    }

    result := 1
    for i := 2; i <= n; i++ {
        result *= i
    }
    return result, nil
}

对应的测试文件:

// calculator_test.go
package main

import (
    "testing"
)

func TestCalculator_Add(t *testing.T) {
    calc := &Calculator{}
    result := calc.Add(2.5, 3.7)
    expected := 6.2

    if result != expected {
        t.Errorf("Add(2.5, 3.7) = %f; want %f", result, expected)
    }
}

func TestCalculator_Subtract(t *testing.T) {
    calc := &Calculator{}
    result := calc.Subtract(10.0, 3.0)
    expected := 7.0

    if result != expected {
        t.Errorf("Subtract(10.0, 3.0) = %f; want %f", result, expected)
    }
}

func TestCalculator_Multiply(t *testing.T) {
    calc := &Calculator{}
    result := calc.Multiply(4.0, 2.5)
    expected := 10.0

    if result != expected {
        t.Errorf("Multiply(4.0, 2.5) = %f; want %f", result, expected)
    }
}

func TestCalculator_Divide(t *testing.T) {
    calc := &Calculator{}

    // 测试正常除法
    result, err := calc.Divide(10.0, 2.0)
    if err != nil {
        t.Errorf("Divide(10.0, 2.0) returned error: %v", err)
    }

    expected := 5.0
    if result != expected {
        t.Errorf("Divide(10.0, 2.0) = %f; want %f", result, expected)
    }

    // 测试除零错误
    _, err = calc.Divide(10.0, 0.0)
    if err == nil {
        t.Error("Divide(10.0, 0.0) should return error")
    }
}

func TestCalculator_IsEven(t *testing.T) {
    calc := &Calculator{}

    tests := []struct {
        input    int
        expected bool
    }{
        {2, true},
        {3, false},
        {0, true},
        {-2, true},
        {-3, false},
    }

    for _, test := range tests {
        result := calc.IsEven(test.input)
        if result != test.expected {
            t.Errorf("IsEven(%d) = %t; want %t", test.input, result, test.expected)
        }
    }
}

// 注意:我们故意没有测试 Power, Sqrt, 和 Factorial 方法
// 这样可以看到覆盖率的差异

运行覆盖率分析 #

# 基本覆盖率检查
go test -cover

# 输出示例:
# PASS
# coverage: 66.7% of statements

生成详细覆盖率报告 #

# 生成覆盖率配置文件
go test -coverprofile=coverage.out

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

# 输出示例:
# calculator.go:8:   Add         100.0%
# calculator.go:13:  Subtract    100.0%
# calculator.go:18:  Multiply    100.0%
# calculator.go:23:  Divide      100.0%
# calculator.go:30:  Power       0.0%
# calculator.go:35:  Sqrt        0.0%
# calculator.go:42:  IsEven      100.0%
# calculator.go:47:  Factorial   0.0%
# total:             (statement) 66.7%

生成 HTML 覆盖率报告 #

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

# 在浏览器中打开
# 绿色表示已覆盖的代码
# 红色表示未覆盖的代码

不同类型的覆盖率 #

Go 支持多种覆盖率模式:

# 语句覆盖率(默认)
go test -covermode=set -coverprofile=coverage.out

# 计数覆盖率(显示每行执行次数)
go test -covermode=count -coverprofile=coverage.out

# 原子覆盖率(适用于并发测试)
go test -covermode=atomic -coverprofile=coverage.out

完善测试以提高覆盖率 #

让我们添加缺失的测试:

func TestCalculator_Power(t *testing.T) {
    calc := &Calculator{}

    tests := []struct {
        base, exponent, expected float64
    }{
        {2.0, 3.0, 8.0},
        {5.0, 2.0, 25.0},
        {10.0, 0.0, 1.0},
        {2.0, -1.0, 0.5},
    }

    for _, test := range tests {
        result := calc.Power(test.base, test.exponent)
        if result != test.expected {
            t.Errorf("Power(%f, %f) = %f; want %f",
                test.base, test.exponent, result, test.expected)
        }
    }
}

func TestCalculator_Sqrt(t *testing.T) {
    calc := &Calculator{}

    // 测试正常情况
    result, err := calc.Sqrt(9.0)
    if err != nil {
        t.Errorf("Sqrt(9.0) returned error: %v", err)
    }

    expected := 3.0
    if result != expected {
        t.Errorf("Sqrt(9.0) = %f; want %f", result, expected)
    }

    // 测试负数错误
    _, err = calc.Sqrt(-1.0)
    if err == nil {
        t.Error("Sqrt(-1.0) should return error")
    }
}

func TestCalculator_Factorial(t *testing.T) {
    calc := &Calculator{}

    tests := []struct {
        input    int
        expected int
        hasError bool
    }{
        {0, 1, false},
        {1, 1, false},
        {5, 120, false},
        {-1, 0, true},
    }

    for _, test := range tests {
        result, err := calc.Factorial(test.input)

        if test.hasError {
            if err == nil {
                t.Errorf("Factorial(%d) should return error", test.input)
            }
        } else {
            if err != nil {
                t.Errorf("Factorial(%d) returned error: %v", test.input, err)
            }
            if result != test.expected {
                t.Errorf("Factorial(%d) = %d; want %d", test.input, result, test.expected)
            }
        }
    }
}

现在运行覆盖率测试应该显示 100% 的覆盖率。

基准测试 #

基准测试用于测量代码的性能,帮助识别性能瓶颈和优化机会。

基本基准测试 #

基准测试函数必须以 Benchmark 开头,并接受 *testing.B 参数:

// benchmark_test.go
package main

import (
    "testing"
    "strings"
    "fmt"
)

// 测试字符串连接的不同方法
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        result := ""
        for j := 0; j < 100; j++ {
            result += "hello"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        for j := 0; j < 100; j++ {
            builder.WriteString("hello")
        }
        _ = builder.String()
    }
}

func BenchmarkStringJoin(b *testing.B) {
    for i := 0; i < b.N; i++ {
        parts := make([]string, 100)
        for j := 0; j < 100; j++ {
            parts[j] = "hello"
        }
        _ = strings.Join(parts, "")
    }
}

// 测试计算器方法的性能
func BenchmarkCalculator_Add(b *testing.B) {
    calc := &Calculator{}
    for i := 0; i < b.N; i++ {
        calc.Add(1.5, 2.5)
    }
}

func BenchmarkCalculator_Factorial(b *testing.B) {
    calc := &Calculator{}
    for i := 0; i < b.N; i++ {
        calc.Factorial(10)
    }
}

运行基准测试 #

# 运行所有基准测试
go test -bench=.

# 运行特定基准测试
go test -bench=BenchmarkStringConcat

# 运行匹配模式的基准测试
go test -bench="String.*"

# 显示内存分配统计
go test -bench=. -benchmem

# 运行多次以获得更准确的结果
go test -bench=. -count=5

# 设置基准测试运行时间
go test -bench=. -benchtime=10s

基准测试输出解读 #

BenchmarkStringConcat-8         2000    750000 ns/op    503992 B/op    99 allocs/op
BenchmarkStringBuilder-8       20000     75000 ns/op      1024 B/op     2 allocs/op
BenchmarkStringJoin-8          30000     50000 ns/op      1024 B/op     1 allocs/op

输出说明:

  • BenchmarkStringConcat-8: 函数名和 GOMAXPROCS 值
  • 2000: 执行次数(b.N 的值)
  • 750000 ns/op: 每次操作的平均耗时(纳秒)
  • 503992 B/op: 每次操作分配的字节数
  • 99 allocs/op: 每次操作的内存分配次数

高级基准测试技巧 #

1. 子基准测试 #

func BenchmarkSortAlgorithms(b *testing.B) {
    sizes := []int{100, 1000, 10000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("BubbleSort-%d", size), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                data := generateRandomSlice(size)
                bubbleSort(data)
            }
        })

        b.Run(fmt.Sprintf("QuickSort-%d", size), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                data := generateRandomSlice(size)
                quickSort(data)
            }
        })
    }
}

func generateRandomSlice(size int) []int {
    slice := make([]int, size)
    for i := range slice {
        slice[i] = rand.Intn(1000)
    }
    return slice
}

2. 基准测试设置和清理 #

func BenchmarkDatabaseOperation(b *testing.B) {
    // 设置阶段(不计入基准测试时间)
    db := setupTestDatabase()
    defer db.Close()

    b.ResetTimer() // 重置计时器,排除设置时间

    for i := 0; i < b.N; i++ {
        // 被测试的操作
        performDatabaseQuery(db)
    }
}

3. 并行基准测试 #

func BenchmarkParallelOperation(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // 并行执行的操作
            expensiveOperation()
        }
    })
}

4. 基准测试比较 #

func BenchmarkMapVsSlice(b *testing.B) {
    data := generateTestData(1000)

    b.Run("Map", func(b *testing.B) {
        m := make(map[string]int)
        for k, v := range data {
            m[k] = v
        }

        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            for k := range data {
                _ = m[k]
            }
        }
    })

    b.Run("Slice", func(b *testing.B) {
        type pair struct {
            key   string
            value int
        }

        slice := make([]pair, 0, len(data))
        for k, v := range data {
            slice = append(slice, pair{k, v})
        }

        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            for _, target := range slice {
                for _, p := range slice {
                    if p.key == target.key {
                        _ = p.value
                        break
                    }
                }
            }
        }
    })
}

性能分析和优化 #

使用 pprof 进行性能分析 #

func BenchmarkWithProfiling(b *testing.B) {
    for i := 0; i < b.N; i++ {
        expensiveFunction()
    }
}
# 生成 CPU 性能分析文件
go test -bench=BenchmarkWithProfiling -cpuprofile=cpu.prof

# 生成内存性能分析文件
go test -bench=BenchmarkWithProfiling -memprofile=mem.prof

# 使用 pprof 分析
go tool pprof cpu.prof
go tool pprof mem.prof

基准测试最佳实践 #

1. 避免编译器优化 #

// 错误:编译器可能优化掉未使用的结果
func BenchmarkBad(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2) // 结果未使用,可能被优化
    }
}

// 正确:保存结果防止优化
var result int

func BenchmarkGood(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = Add(1, 2)
    }
    result = r // 防止编译器优化
}

2. 预分配内存 #

func BenchmarkSliceAppend(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := make([]int, 0, 1000) // 预分配容量
        for j := 0; j < 1000; j++ {
            slice = append(slice, j)
        }
    }
}

3. 使用基准测试辅助函数 #

func benchmarkFibonacci(i int, b *testing.B) {
    for n := 0; n < b.N; n++ {
        fibonacci(i)
    }
}

func BenchmarkFibonacci1(b *testing.B)  { benchmarkFibonacci(1, b) }
func BenchmarkFibonacci2(b *testing.B)  { benchmarkFibonacci(2, b) }
func BenchmarkFibonacci3(b *testing.B)  { benchmarkFibonacci(3, b) }
func BenchmarkFibonacci10(b *testing.B) { benchmarkFibonacci(10, b) }
func BenchmarkFibonacci20(b *testing.B) { benchmarkFibonacci(20, b) }

测试覆盖率和基准测试的集成 #

可以同时运行测试覆盖率和基准测试:

# 同时运行单元测试、基准测试和覆盖率分析
go test -bench=. -cover -coverprofile=coverage.out

# 生成完整的测试报告
go test -bench=. -benchmem -cover -coverprofile=coverage.out -v

通过掌握测试覆盖率分析和基准测试,您可以更好地评估代码质量和性能特征。在下一节中,我们将通过一个完整的命令行工具开发项目来综合应用这些测试技能。