莫度编程网

技术文章干货、编程学习教程与开发工具分享

Go 单元测试全面指南_go单元测试框架

Go 单元测试全面指南

1. 单元测试基础概念

单元测试是对软件中最小可测试单元(通常是函数或方法)进行检查和验证的过程。在 Go 中,单元测试有以下特点:

  • 测试文件以 _test.go 结尾
  • 测试函数以 Test 开头
  • 测试函数接收 *testing.T 参数
  • 使用 go test 命令运行测试

2. 测试文件结构

一个典型的 Go 测试文件结构如下:

project/
├── main.go       # 源代码
└── main_test.go  # 测试代码

3. 基本测试示例

被测代码 (math.go)

package math

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

func Subtract(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; want %d", result, expected)
    }
}

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

4. 测试运行方式

# 运行所有测试
go test

# 显示详细输出
go test -v

# 运行特定测试
go test -v -run TestAdd

# 运行测试并显示覆盖率
go test -cover

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

5. 表驱动测试

表驱动测试是一种将测试用例组织成表格形式的测试方法:

func TestAdd_TableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -1, -2},
        {"mixed numbers", -1, 1, 0},
        {"zero values", 0, 0, 0},
    }

    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; want %d", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

6. 子测试与并行测试

func TestMultiply(t *testing.T) {
    t.Parallel() // 标记测试可以并行运行
    
    t.Run("positive numbers", func(t *testing.T) {
        if Multiply(2, 3) != 6 {
            t.Error("expected 6")
        }
    })
    
    t.Run("negative numbers", func(t *testing.T) {
        if Multiply(-2, -3) != 6 {
            t.Error("expected 6")
        }
    })
}

7. 测试辅助函数

func assertEqual(t *testing.T, result, expected int) {
    t.Helper() // 标记为辅助函数,错误报告会跳过此函数
    if result != expected {
        t.Errorf("got %d, want %d", result, expected)
    }
}

func TestDivide(t *testing.T) {
    assertEqual(t, Divide(6, 3), 2)
    assertEqual(t, Divide(10, 2), 5)
}

8. 基准测试

基准测试用于测量代码性能:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

运行基准测试:

go test -bench=.
go test -bench=. -benchmem # 包含内存分配信息

9. 示例测试

示例测试既作为测试也作为文档:

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

10. 测试初始化与清理

func TestMain(m *testing.M) {
    // 测试前初始化
    setup()
    
    // 运行测试
    code := m.Run()
    
    // 测试后清理
    teardown()
    
    // 退出
    os.Exit(code)
}

func setup() {
    fmt.Println("测试初始化")
}

func teardown() {
    fmt.Println("测试清理")
}

11. 测试替身(Mock/Stub)

使用接口实现测试替身:

type Database interface {
    GetUser(id int) (string, error)
}

type RealDB struct{}

func (db *RealDB) GetUser(id int) (string, error) {
    // 实际数据库操作
    return "real user", nil
}

type MockDB struct{}

func (db *MockDB) GetUser(id int) (string, error) {
    return "mock user", nil
}

func TestGetUserName(t *testing.T) {
    db := &MockDB{}
    name, _ := db.GetUser(1)
    if name != "mock user" {
        t.Errorf("expected mock user, got %s", name)
    }
}

12. 测试HTTP服务

func TestHTTPHandler(t *testing.T) {
    req, err := http.NewRequest("GET", "/hello", nil)
    if err != nil {
        t.Fatal(err)
    }
    
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(HelloHandler)
    
    handler.ServeHTTP(rr, req)
    
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
    
    expected := "Hello, World!"
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

13. 测试命令行工具

func TestCLI(t *testing.T) {
    cmd := exec.Command("./myapp", "arg1", "arg2")
    output, err := cmd.CombinedOutput()
    if err != nil {
        t.Fatalf("command failed: %v\n%s", err, output)
    }
    
    expected := "expected output"
    if !strings.Contains(string(output), expected) {
        t.Errorf("unexpected output: got %q, want %q", string(output), expected)
    }
}

14. 测试覆盖率

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

# 查看覆盖率报告
go tool cover -func=coverage.out

# 以HTML形式查看
go tool cover -html=coverage.out

# 测试覆盖率阈值检查
go test -cover -covermode=count -coverpkg=./... -coverprofile=coverage.out
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//'

15. 高级测试技巧

15.1 测试时间相关代码

func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    // 长时间运行的测试代码
}

运行短测试:

go test -short

15.2 测试随机行为

func TestRandomBehavior(t *testing.T) {
    rand.Seed(1) // 固定随机种子以获得可重复的结果
    
    // 测试代码
}

15.3 测试私有函数

虽然不推荐直接测试私有函数,但可以通过以下方式实现:

// 在测试文件中导出私有函数进行测试
var ExportedPrivateFunc = privateFunc

func TestPrivateFunc(t *testing.T) {
    result := ExportedPrivateFunc()
    // 断言
}

16. 测试最佳实践

  1. 保持测试独立:每个测试应该独立运行,不依赖其他测试的状态
  2. 测试命名清晰:使用描述性的测试名称
  3. 测试失败信息明确:错误信息应该清楚地说明期望值和实际值
  4. 避免测试实现细节:测试行为而非实现
  5. 保持测试快速:单元测试应该快速执行
  6. 覆盖率合理:追求有意义的覆盖率而非100%
  7. 测试边界条件:包括零值、空值、最大值、最小值等
  8. 定期重构测试:随着代码演进,测试也需要维护

通过掌握这些单元测试技术,你可以为 Go 项目构建可靠的测试套件,确保代码质量并支持持续集成和部署流程。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言

    Powered By Z-BlogPHP 1.7.4

    蜀ICP备2024111239号-43