目录

Go 测试

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

单元测试基础

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

// 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
}
// 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 后缀(黑盒测试)
// 白盒测试:同包,可以访问未导出的标识符
package mypackage

import "testing"

func TestInternal(t *testing.T) {
	// 可以直接调用未导出的函数
	result := internalHelper()
	if result == "" {
		t.Error("internalHelper 返回空字符串")
	}
}
// 黑盒测试:使用 _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 中最推荐的测试模式,将多组测试数据组织成表格,用循环统一执行。

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 创建命名的子测试,可以独立运行和过滤。

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

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 开头。

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 查看测试覆盖率。

# 查看覆盖率
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 功能。

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 等),让测试可以独立运行。

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 功能。好的测试应该覆盖正常路径、边界条件和错误处理。