jee said

Go语言单元测试

unclejee & AI
1529 words
8min read
Go语言单元测试

Go语言单元测试

Go语言内置了强大的测试框架,使得编写和运行测试变得简单而高效。本文将介绍Go语言的单元测试最佳实践。

基本测试

测试文件命名

Go语言的测试文件需要以_test.go结尾,并放在与被测试文件相同的包中。

// 被测试文件:math.go
// 测试文件:math_test.go

基本测试结构

// math.go
package math

func Add(a, b int) int {
    return a + b
}
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; 期望 %d", result, expected)
    }
}

运行测试

# 运行所有测试
go test

# 运行指定包的测试
go test ./math

# 运行指定测试
go test -run TestAdd

# 显示详细输出
go test -v

表驱动测试

表驱动测试是Go语言中常见的测试模式,可以减少重复代码。

基本表驱动测试

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正数相加", 2, 3, 5},
        {"负数相加", -2, -3, -5},
        {"零相加", 0, 0, 0},
        {"混合相加", 5, -3, 2},
    }

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

复杂表驱动测试

type DivideTest struct {
    name        string
    a, b        int
    expected    int
    expectError bool
}

func TestDivide(t *testing.T) {
    tests := []DivideTest{
        {"正常除法", 10, 2, 5, false},
        {"除以零", 10, 0, 0, true},
        {"负数除法", -10, 2, -5, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Divide(tt.a, tt.b)

            if tt.expectError {
                if err == nil {
                    t.Errorf("期望错误但没有错误")
                }
            } else {
                if err != nil {
                    t.Errorf("不期望错误但得到: %v", err)
                }
                if result != tt.expected {
                    t.Errorf("Divide(%d, %d) = %d; 期望 %d",
                        tt.a, tt.b, result, tt.expected)
                }
            }
        })
    }
}

测试辅助函数

t.Helper()

func assertEqual(t *testing.T, got, expected int) {
    t.Helper()  // 标记为辅助函数
    if got != expected {
        t.Errorf("期望 %d,得到 %d", expected, got)
    }
}

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

自定义辅助函数

// 辅助函数文件:testutil.go
package math

func AssertEqual(t *testing.T, got, expected int) {
    t.Helper()
    if got != expected {
        t.Errorf("期望 %d,得到 %d", expected, got)
    }
}

// 测试文件:math_test.go
package math

func TestAdd(t *testing.T) {
    AssertEqual(t, Add(2, 3), 5)
}

测试覆盖率

生成覆盖率报告

# 生成覆盖率报告
go test -coverprofile=coverage.out

# 查看覆盖率
go tool cover -html=coverage.out

设置覆盖率阈值

# 设置最低覆盖率要求
go test -coverprofile=coverage.out -covermode=atomic

# 检查覆盖率是否达标
go tool cover -func=coverage.out | grep total

覆盖率过滤

// 在测试文件中添加构建标签
// +build !integration

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("期望 5,得到 %d", result)
    }
}
# 排除集成测试
go test -short

Mock和Stub

接口Mock

// 定义接口
type Database interface {
    GetUser(id int) (*User, error)
}

// Mock实现
type MockDatabase struct {
    users map[int]*User
}

func (m *MockDatabase) GetUser(id int) (*User, error) {
    user, ok := m.users[id]
    if !ok {
        return nil, fmt.Errorf("用户不存在")
    }
    return user, nil
}

// 使用Mock测试
func TestGetUser(t *testing.T) {
    mockDB := &MockDatabase{
        users: map[int]*User{
            1: {ID: 1, Name: "张三"},
        },
    }

    user, err := mockDB.GetUser(1)
    if err != nil {
        t.Errorf("获取用户失败: %v", err)
        return
    }

    if user.Name != "张三" {
        t.Errorf("期望用户名 张三,得到 %s", user.Name)
    }
}

函数Stub

// 原始函数
var getCurrentTime = time.Now

// Stub函数
func setCurrentTime(t time.Time) {
    getCurrentTime = func() time.Time {
        return t
    }
}

// 测试中使用Stub
func TestTimeBasedLogic(t *testing.T) {
    // 设置固定时间
    setCurrentTime(time.Date(2024, 1, 1, 0, 0, 0, time.UTC))

    // 测试基于时间的逻辑
    result := someTimeBasedFunction()

    // 验证结果
    if result != expected {
        t.Errorf("期望 %s,得到 %s", expected, result)
    }
}

基准测试

基本基准测试

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(i, i+1)
    }
}
# 运行基准测试
go test -bench=.

# 运行指定基准测试
go test -bench=BenchmarkAdd

基准测试参数

func BenchmarkAdd(b *testing.B) {
    b.ReportAllocs()  // 报告内存分配

    for i := 0; i < b.N; i++ {
        Add(i, i+1)
    }
}
# 显示内存分配信息
go test -bench=. -benchmem

并发测试

并发测试示例

func TestConcurrentAccess(t *testing.T) {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++
        }()
    }

    wg.Wait()

    if counter != 100 {
        t.Errorf("期望计数器为100,得到 %d", counter)
    }
}

并发基准测试

func BenchmarkConcurrentAdd(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(1, 2)
        }
    })
}

子测试

子测试结构

func TestCalculator(t *testing.T) {
    t.Run("加法", func(t *testing.T) {
        result := Add(2, 3)
        if result != 5 {
            t.Errorf("期望 5,得到 %d", result)
        }
    })

    t.Run("减法", func(t *testing.T) {
        result := Subtract(5, 3)
        if result != 2 {
            t.Errorf("期望 2,得到 %d", result)
        }
    })
}

运行子测试

# 运行特定子测试
go test -run TestCalculator/加法

# 运行所有子测试
go test -run TestCalculator/

测试最佳实践

  1. 使用表驱动测试:减少重复代码,提高可维护性
  2. 测试边界条件:测试空值、零值、最大值等边界情况
  3. 使用辅助函数:减少测试代码的重复
  4. 保持测试独立:每个测试应该独立运行,不依赖其他测试
  5. 使用有意义的测试名称:测试名称应该清楚描述测试内容
  6. 测试错误情况:不仅要测试正常情况,还要测试错误情况
  7. 保持测试快速:单元测试应该快速运行
  8. 使用Mock隔离依赖:使用Mock对象隔离外部依赖

总结

Go语言的测试框架简洁而强大,通过合理的测试策略和最佳实践,可以编写出高质量、可维护的测试代码。掌握Go语言的测试技巧对于提高代码质量至关重要。