1 概述
go test
是 Go 语言内置的测试工具,用于执行测试函数(test function)、基准函数(benchmark function)和示例函数(example function)。它会自动寻找 *_test.go
文件中符合命名规则的函数并执行。
1.1 测试文件命名规范
- 测试文件必须以
_test.go
结尾 - 正常编译操作(
go build
/go install
)会忽略这些文件 - 名称以
_
或.
开头的文件即使后缀是_test.go
也会被忽略
1.2 测试函数类型
- 测试函数 :必须以
TestXXX
开头,接收*testing.T
参数 - 基准函数 :必须以
BenchmarkXXX
开头,接收*testing.B
参数 - 示例函数 :必须以
ExampleXXX
开头,为文档提供示例
2 基本用法和测试模式
2.1 运行测试的基本命令
bash
# 运行当前目录所有测试
go test
# 显示详细输出
go test -v
# 运行所有包(包括子目录)
go test ./...
# 运行特定包测试
go test math
2.2 测试运行模式
go test
有两种运行模式:
-
本地目录模式 :在没有包参数时调用(如
go test
或go test -v
)。此模式下缓存是禁用的。 -
包列表模式 :显示指定包参数时调用(如
go test math
、go test ./...
)。此模式下会缓存成功的测试结果。
2.3 测试缓存管理
bash
# 清理所有缓存,包括测试缓存
go clean -cache
# 只清理测试缓存
go clean -testcache
# 禁用缓存(使用-count=1)
go test -count=1
测试结果在以下情况下会被缓存:相同的测试二进制文件、命令行选项来自可缓存选项集合(-benchtime, -cpu, -list, -parallel, -run, -short, -v)。要禁用缓存,可使用除可缓存选项之外的任何测试选项。
3 测试函数编写与实践
3.1 测试函数结构
go
package mypackage
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Add(1, 2) = %d, expected 3", result)
}
}
3.2 表驱动测试
对于多个测试用例的场景,推荐使用表驱动测试:
go
func TestIsPalindrome(t *testing.T) {
var tests = []struct {
input string
want bool
}{
{"", true},
{"a", true},
{"aa", true},
{"ab", false},
{"kayak", true},
{"A man, a plan, a canal: Panama", true},
}
for _, test := range tests {
if got := IsPalindrome(test.input); got != test.want {
t.Errorf("IsPalindrome(%q) = %v", test.input, got)
}
}
}
3.3 子测试(Subtests)
使用 t.Run()
创建子测试,可以更好地组织测试用例:
go
func TestSumSubTest(t *testing.T) {
t.Run("1+2", func(t *testing.T) {
val := Sum(1, 2)
t.Log("1+2=", val)
})
t.Run("2+3", func(t *testing.T) {
val := Sum(2, 3)
t.Log("2+3=", val)
})
}
3.4 并行测试
对于可以并行执行的测试,使用 t.Parallel()
:
go
func TestSumParallel(t *testing.T) {
t.Parallel()
// 测试逻辑
}
func TestSumParallel2(t *testing.T) {
t.Parallel()
// 测试逻辑
}
使用 -parallel n
指定最大并发数。
3.5 测试初始化与清理
使用 TestMain
作为测试入口点,进行初始化和清理操作:
go
func TestMain(m *testing.M) {
fmt.Printf("测试初始化\n")
// 初始化代码
exitCode := m.Run()
// 清理代码
fmt.Printf("测试清理\n")
os.Exit(exitCode)
}
如果没有在 TestMain
中调用 m.Run()
,除了 TestMain()
以外的其他 test 都不会执行。
4 基准测试(Benchmark)
4.1 基准测试函数结构
go
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(i, i+1)
}
}
4.2 运行基准测试
bash
# 运行所有基准测试
go test -bench=.
# 运行特定基准测试
go test -bench=BenchmarkAdd
# 显示内存分配统计
go test -bench=. -benchmem
# 指定基准测试运行时间
go test -bench=. -benchtime=5s
4.3 基准测试计时器控制
对于需要复杂初始化的基准测试,可以控制计时器:
go
func BenchmarkComplexOperation(b *testing.B) {
// 复杂的初始化操作,不计入基准测试时间
setup()
b.ResetTimer() // 重置计时器
for i := 0; i < b.N; i++ {
complexOperation()
}
}
还可以使用 b.StopTimer()
和 b.StartTimer()
来暂停和恢复计时。
5 常用参数和标志
5.1 测试选择参数
下表总结了常用的测试选择参数:
参数 | 描述 | 示例 |
---|---|---|
-run |
运行匹配正则表达式的测试函数 | go test -run TestAdd |
-bench |
运行匹配正则表达式的基准测试 | go test -bench BenchmarkAdd |
-v |
详细输出,显示所有测试日志 | go test -v |
-count |
运行每个测试/基准测试的次数 | go test -count=3 |
-timeout |
设置测试超时时间 | go test -timeout=30s |
5.2 高级控制参数
bash
# 随机化测试执行顺序
go test -shuffle=on
# 遇到第一个失败测试就停止
go test -failfast
# 设置并行测试数量
go test -parallel=4
# 列出匹配的测试但不执行
go test -list="TestAdd"
5.3 覆盖率分析
bash
# 启用覆盖率分析
go test -cover
# 生成覆盖率文件
go test -coverprofile=cover.out
# 查看详细的覆盖率报告
go tool cover -html=cover.out
# 设置覆盖率模式
go test -covermode=count
覆盖率模式有三种:
set
(布尔值):语句是否运行count
(整数):语句运行了多少次atomic
(整数):支持并发场景的计数
6 性能分析
Go test 支持生成多种性能分析文件:
6.1 生成性能分析数据
bash
# CPU 性能分析
go test -cpuprofile=cpu.out
# 内存分配分析
go test -memprofile=mem.out
# 阻塞分析
go test -blockprofile=block.out
# 执行跟踪
go test -trace=trace.out
6.2 分析性能数据
生成分析文件后,可以使用 go tool pprof
进行分析:
bash
# 分析 CPU 性能
go tool pprof cpu.out
# 分析内存分配
go tool pprof mem.out
# 查看执行跟踪
go tool trace trace.out
7 高级特性与技巧
7.1 测试二进制文件操作
bash
# 编译测试二进制文件但不运行
go test -c -o test_binary
# 运行已编译的测试二进制文件
./test_binary -test.v -test.run=TestAdd
# 编译带调试信息的测试二进制文件
go test -c -gcflags="all=-N -l" -o debug_binary
7.2 JSON 格式输出
对于自动化测试,可以输出 JSON 格式的结果:
bash
go test -json
7.3 传递参数给测试二进制文件
使用 -args
将参数传递给测试程序:
go
func TestArgs(t *testing.T) {
if !flag.Parsed() {
flag.Parse()
}
argList := flag.Args()
for _, arg := range argList {
if arg == "cloud" {
t.Log("Running in cloud mode.")
}
}
}
运行测试时传递参数:
bash
go test -run TestArgs -v -args "cloud"
8 实际应用示例
8.1 完整的测试文件示例
go
package example
import (
"flag"
"fmt"
"testing"
"time"
)
var print bool
func init() {
flag.BoolVar(&print, "p", false, "print test log")
}
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Add(1, 2) = %d, expected 3", result)
}
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(i, i+1)
}
}
func ExampleAdd() {
sum := Add(1, 2)
fmt.Println(sum)
// Output: 3
}
func TestMain(m *testing.M) {
flag.Parse()
fmt.Printf("测试初始化\n")
exitCode := m.Run()
fmt.Printf("测试清理\n")
os.Exit(exitCode)
}
8.2 测试 HTTP 处理程序
go
package http_test
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/health", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(HealthHandler)
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 := `{"status":"healthy"}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
9 最佳实践
-
保持测试独立:每个测试应该独立于其他测试,不依赖执行顺序。
-
使用有意义的测试名称:测试名称应该清晰描述被测试的功能。
-
测试错误情况:不仅要测试正常情况,还要测试错误和边界情况。
-
避免睡眠 :在测试中避免使用
time.Sleep
,使用同步机制或测试助手函数。 -
并行化长时间测试 :对于长时间运行的测试,使用
t.Parallel()
来加速测试执行。 -
定期检查测试覆盖率:使用覆盖率工具确保测试足够全面。
-
保持测试快速:测试应该快速执行,以便频繁运行。
通过掌握 go test
的各种特性和技巧,您可以构建健壮、可维护的测试套件,确保代码质量并促进持续集成流程。