# Go 测试

测试是保证代码质量的重要手段。Go 语言内置了 `testing` 包，提供了单元测试、基准测试和示例测试的能力，无需引入第三方测试框架。配合表驱动测试、子测试等技巧，可以编写结构清晰、易于维护的测试代码。

## 单元测试基础

Go 的测试文件必须以 `_test.go` 结尾，测试函数以 `Test` 开头，参数为 `*testing.T`。

```go
// math.go
package math

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

func Divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, fmt.Errorf("除数不能为零")
	}
	return a / b, nil
}
```

```go
// math_test.go
package math

import (
	"testing"
)

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

func TestAddNegative(t *testing.T) {
	result := Add(-1, -2)
	if result != -3 {
		t.Errorf("Add(-1, -2) = %d; 期望 -3", result)
	}
}

func TestDivide(t *testing.T) {
	result, err := Divide(10, 2)
	if err != nil {
		t.Fatalf("意外的错误: %v", err)
	}
	if result != 5 {
		t.Errorf("Divide(10, 2) = %f; 期望 5.0", result)
	}
}

func TestDivideByZero(t *testing.T) {
	_, err := Divide(10, 0)
	if err == nil {
		t.Error("期望错误，但得到了 nil")
	}
}
```

`testing.T` 提供的方法：
- `t.Errorf`：报告错误但继续执行
- `t.Fatalf`：报告错误并立即停止当前测试
- `t.Error`：等价于 `Errorf` 的简化版
- `t.Log`：输出日志信息
- `t.Skip`：跳过测试

## 测试文件命名约定

Go 的测试有严格的命名约定：
- 测试文件：`xxx_test.go`（编译器自动识别）
- 测试函数：`TestXxx(t *testing.T)`
- 包名：通常与被测代码相同（白盒测试）或加 `_test` 后缀（黑盒测试）

```go
// 白盒测试：同包，可以访问未导出的标识符
package mypackage

import "testing"

func TestInternal(t *testing.T) {
	// 可以直接调用未导出的函数
	result := internalHelper()
	if result == "" {
		t.Error("internalHelper 返回空字符串")
	}
}
```

```go
// 黑盒测试：使用 _test 包名，只能访问导出的标识符
package mypackage_test

import (
	"testing"
	"mypackage"
)

func TestExported(t *testing.T) {
	result := mypackage.PublicFunction()
	if result == nil {
		t.Error("PublicFunction 返回 nil")
	}
}
```

## 表驱动测试（Table-Driven Tests）

表驱动测试是 Go 中最推荐的测试模式，将多组测试数据组织成表格，用循环统一执行。

```go
package main

import "testing"

func Abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

func TestAbs(t *testing.T) {
	// 定义测试表
	tests := []struct {
		name     string // 测试名称
		input    int    // 输入
		expected int    // 期望输出
	}{
		{"正数", 5, 5},
		{"负数", -3, 3},
		{"零", 0, 0},
		{"大负数", -100, 100},
		{"大正数", 999, 999},
	}

	// 遍历测试表
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := Abs(tt.input)
			if result != tt.expected {
				t.Errorf("Abs(%d) = %d; 期望 %d", tt.input, result, tt.expected)
			}
		})
	}
}
```

表驱动测试的优势：
1. 新增测试用例只需添加一行数据
2. 测试逻辑只写一次
3. 配合子测试，失败时能精确定位到哪个用例

## 子测试（t.Run）

`t.Run` 创建命名的子测试，可以独立运行和过滤。

```go
package main

import "testing"

func TestStringOperations(t *testing.T) {
	// 子测试 1
	t.Run("空字符串", func(t *testing.T) {
		s := ""
		if len(s) != 0 {
			t.Errorf("空字符串长度应为 0，实际为 %d", len(s))
		}
	})

	// 子测试 2
	t.Run("字符串拼接", func(t *testing.T) {
		result := "Hello" + " " + "World"
		expected := "Hello World"
		if result != expected {
			t.Errorf("拼接结果: %q; 期望 %q", result, expected)
		}
	})

	// 子测试 3
	t.Run("字符串包含", func(t *testing.T) {
		s := "Hello, Go!"
		if !contains(s, "Go") {
			t.Errorf("%q 应包含 'Go'", s)
		}
	})
}

func contains(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
```

运行特定子测试：`go test -run TestStringOperations/空字符串`

## 基准测试（Benchmark）

基准测试用于衡量代码性能，函数名以 `Benchmark` 开头，参数为 `*testing.B`。

```go
package main

import (
	"fmt"
	"testing"
)

func Fibonacci(n int) int {
	if n <= 1 {
		return n
	}
	return Fibonacci(n-1) + Fibonacci(n-2)
}

func FibonacciIterative(n int) int {
	if n <= 1 {
		return n
	}
	a, b := 0, 1
	for i := 2; i <= n; i++ {
		a, b = b, a+b
	}
	return b
}

// 基准测试：递归版本
func BenchmarkFibRecursive(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Fibonacci(20)
	}
}

// 基准测试：迭代版本
func BenchmarkFibIterative(b *testing.B) {
	for i := 0; i < b.N; i++ {
		FibonacciIterative(20)
	}
}

func main() {
	// 手动运行基准测试
	result1 := testing.Benchmark(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			Fibonacci(20)
		}
	})
	fmt.Printf("递归版本: %v/op, %d 次迭代\n", result1.NsPerOp(), result1.N)

	result2 := testing.Benchmark(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			FibonacciIterative(20)
		}
	})
	fmt.Printf("迭代版本: %v/op, %d 次迭代\n", result2.NsPerOp(), result2.N)
}
```

运行基准测试：`go test -bench=. -benchmem`

`b.N` 由测试框架自动调整，确保测试运行足够多次以获得稳定的结果。`-benchmem` 标志显示内存分配信息。

## 示例测试（Example）

示例测试既是文档又是可执行的测试，函数名以 `Example` 开头。

```go
package main

import (
	"fmt"
)

// Add 两数相加
func Add(a, b int) int {
	return a + b
}

// ExampleAdd 是 Add 函数的示例测试
func ExampleAdd() {
	fmt.Println(Add(2, 3))
	fmt.Println(Add(-1, 1))
	// Output:
	// 5
	// 0
}

// ExampleAdd_negative 带后缀的示例
func ExampleAdd_negative() {
	fmt.Println(Add(-5, -3))
	// Output:
	// -8
}

func main() {
	ExampleAdd()
	ExampleAdd_negative()
}
```

`// Output:` 注释是必须的，`go test` 会将实际输出与注释中的期望输出进行比较。示例函数会出现在 `go doc` 的文档中。

## 测试覆盖率

使用 `go test -cover` 查看测试覆盖率。

```bash
# 查看覆盖率
go test -cover

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

# 按函数查看覆盖率
go test -coverprofile=coverage.out
go tool cover -func=coverage.out
```

覆盖率只是参考指标，100% 覆盖率不代表没有 bug。关注关键路径和边界条件的测试比追求覆盖率数字更重要。

## testify 库简介

`testify` 是最流行的 Go 测试辅助库，提供断言和 mock 功能。

```go
package main

import (
	"fmt"
	"testing"
)

// 模拟 testify 的 assert 功能
func assertEqual(t *testing.T, expected, actual interface{}) {
	if expected != actual {
		t.Errorf("期望 %v, 实际 %v", expected, actual)
	}
}

func assertNoError(t *testing.T, err error) {
	if err != nil {
		t.Fatalf("意外的错误: %v", err)
	}
}

func assertContains(t *testing.T, s, substr string) {
	found := false
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("%q 不包含 %q", s, substr)
	}
}

// 使用自定义断言的测试
func TestWithAssertions(t *testing.T) {
	// 等价于 testify 的 assert.Equal
	assertEqual(t, 5, 2+3)

	// 等价于 testify 的 assert.NoError
	assertNoError(t, nil)

	// 等价于 testify 的 assert.Contains
	assertContains(t, "Hello, World!", "World")
}

func main() {
	fmt.Println("运行测试: go test -v")
	fmt.Println("\ntestify 常用功能:")
	fmt.Println("  assert.Equal(t, expected, actual)")
	fmt.Println("  assert.NoError(t, err)")
	fmt.Println("  assert.Contains(t, s, substr)")
	fmt.Println("  assert.Nil(t, obj)")
	fmt.Println("  require.Equal(t, expected, actual) // 失败立即停止")
}
```

安装 testify：`go get github.com/stretchr/testify`

testify 的 `assert` 包在失败时继续执行，`require` 包在失败时立即停止测试。

## mock 测试简介

mock 用于模拟外部依赖（数据库、API 等），让测试可以独立运行。

```go
package main

import "fmt"

// UserRepository 定义用户存储接口
type UserRepository interface {
	FindUser(id int) (string, error)
}

// MockUserRepository 模拟实现
type MockUserRepository struct {
	users map[int]string
}

func (m *MockUserRepository) FindUser(id int) (string, error) {
	name, ok := m.users[id]
	if !ok {
		return "", fmt.Errorf("用户 %d 不存在", id)
	}
	return name, nil
}

// UserService 使用 Repository 的业务逻辑
type UserService struct {
	repo UserRepository
}

func (s *UserService) GetUserName(id int) string {
	name, err := s.repo.FindUser(id)
	if err != nil {
		return "未知用户"
	}
	return name
}

func main() {
	// 使用 mock 进行测试
	mockRepo := &MockUserRepository{
		users: map[int]string{
			1: "张三",
			2: "李四",
		},
	}

	service := &UserService{repo: mockRepo}

	// 测试场景 1：用户存在
	name := service.GetUserName(1)
	fmt.Printf("用户 1: %s\n", name) // 张三

	// 测试场景 2：用户不存在
	name2 := service.GetUserName(99)
	fmt.Printf("用户 99: %s\n", name2) // 未知用户
}
```

mock 的核心思想：通过接口隔离依赖，测试时注入 mock 实现。Go 的接口是隐式实现的，创建 mock 非常简单。

## 总结

Go 内置的 `testing` 包提供了完整的测试能力。测试文件以 `_test.go` 命名，测试函数以 `Test` 开头。表驱动测试是最推荐的模式，配合子测试可以组织清晰的测试结构。基准测试（`Benchmark`）用于性能分析，示例测试（`Example`）兼作文档。`go test -cover` 查看覆盖率。`testify` 库提供更丰富的断言和 mock 功能。好的测试应该覆盖正常路径、边界条件和错误处理。

