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)
}
})
}
}表驱动测试的优势:
- 新增测试用例只需添加一行数据
- 测试逻辑只写一次
- 配合子测试,失败时能精确定位到哪个用例
子测试(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 功能。好的测试应该覆盖正常路径、边界条件和错误处理。