Go语言自带了 testing
测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能。
单元测试是针对程序中最小可测试单元(通常是函数或方法)的测试。在Go中,这通常指对特定函数或方法的输入和输出进行验证,确保它们按预期工作。单元测试的目标是:
Go的测试文件遵循以下命名约定:
_test.go
结尾Test
开头例如,如果你有一个名为calculator.go
的文件,对应的测试文件应命名为calculator_test.go
。
Test
开头,后跟大写字母开头的单词*testing.T
类型的参数func TestXxx(t *testing.T) {
// 测试代码
}
// calculator.go
package calculator
// Add 返回两个整数的和
func Add(a, b int) int {
return a + b
}
// Subtract 返回两个整数的差
func Subtract(a, b int) int {
return a - b
}
// Multiply 返回两个整数的乘积
func Multiply(a, b int) int {
return a * b
}
// Divide 返回两个整数的商,如果除数为0则panic
func Divide(a, b int) int {
if b == 0 {
panic("除数不能为0")
}
return a / b
}
现在,为这些函数编写测试:
// calculator_test.go
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
result := Add(3, 2)
expected := 5
if result != expected {
t.Errorf("Add(3, 2) = %d; 期望 %d", result, expected)
}
}
func TestSubtract(t *testing.T) {
result := Subtract(5, 2)
expected := 3
if result != expected {
t.Errorf("Subtract(5, 2) = %d; 期望 %d", result, expected)
}
}
func TestMultiply(t *testing.T) {
result := Multiply(4, 3)
expected := 12
if result != expected {
t.Errorf("Multiply(4, 3) = %d; 期望 %d", result, expected)
}
}
func TestDivide(t *testing.T) {
result := Divide(6, 2)
expected := 3
if result != expected {
t.Errorf("Divide(6, 2) = %d; 期望 %d", result, expected)
}
}
func TestDivideByZero(t *testing.T) {
// 使用匿名函数封装可能panic的操作
defer func() {
if r := recover(); r != nil {
t.Errorf("%s", r)
}
}()
Divide(6, 0)
}
可以使用go test
命令运行测试:
go test # 运行当前包中的所有测试
go test -v # 详细模式,显示每个测试函数的结果
go test ./... # 运行当前目录及其子目录中的所有测试
go test -run TestAdd # 只运行名称匹配"TestAdd"的测试
使用go test -v ygang.top/demo/calculator
在详细模式下进行测试,得到如下结果
yanggang@MacBook demo % go test -v ygang.top/demo/calculator
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestSubtract
--- PASS: TestSubtract (0.00s)
=== RUN TestMultiply
--- PASS: TestMultiply (0.00s)
=== RUN TestDivide
--- PASS: TestDivide (0.00s)
=== RUN TestDivideByZero
calculator_test.go:48: 除数不能为0
--- FAIL: TestDivideByZero (0.00s)
FAIL
FAIL ygang.top/demo/calculator 0.353s
FAIL
Go社区推崇"表格驱动测试"的模式,这种模式通过创建测试用例表格,使测试更加简洁和可维护。
下面以表格驱动的方式对 Add
方法进行测试
表格驱动测试的主要优势:
func TestAdd_TableDriven(t *testing.T) {
// 定义测试用例表
tests := []struct {
name string // 测试名称
a, b int // 输入参数
expected int // 期望结果
}{
{"正数相加", 3, 2, 5},
{"负数相加", -3, -2, -5},
{"正负相加", 3, -2, 1},
{"零值处理", 0, 0, 0},
{"大数相加", 1000000, 1000000, 2000000},
}
// 遍历测试用例表
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; 期望 %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
执行go test ygang.top/demo/calculator -v -run TestAdd_TableDriven
yanggang@MacBook demo % go test ygang.top/demo/calculator -v -run TestAdd_TableDriven
=== RUN TestAdd_TableDriven
=== RUN TestAdd_TableDriven/正数相加
=== RUN TestAdd_TableDriven/负数相加
=== RUN TestAdd_TableDriven/正负相加
=== RUN TestAdd_TableDriven/零值处理
=== RUN TestAdd_TableDriven/大数相加
--- PASS: TestAdd_TableDriven (0.00s)
--- PASS: TestAdd_TableDriven/正数相加 (0.00s)
--- PASS: TestAdd_TableDriven/负数相加 (0.00s)
--- PASS: TestAdd_TableDriven/正负相加 (0.00s)
--- PASS: TestAdd_TableDriven/零值处理 (0.00s)
--- PASS: TestAdd_TableDriven/大数相加 (0.00s)
PASS
ok ygang.top/demo/calculator (cached)
testing.T
类型提供了多种断言和控制测试流程的方法:
func TestWithHelpers(t *testing.T) {
// 1. Fatal和Fatalf - 报告失败并立即停止测试执行
if !checkSetup() {
t.Fatal("环境设置失败,无法继续测试")
}
// 2. Error和Errorf - 报告失败但继续执行测试
result := Multiply(4, 3)
if result != 12 {
t.Errorf("Multiply(4, 3) = %d; 期望 12", result)
}
// 3. Log和Logf - 记录测试信息(仅在详细模式或测试失败时显示)
t.Log("乘法测试完成")
// 4. Skip和Skipf - 跳过当前测试(例如特定环境不适用)
if testing.Short() {
t.Skip("在短模式下跳过此测试")
}
// 5. Helper - 标记函数为辅助函数(错误报告中正确标注行号)
t.Helper()
// 继续测试...
}
良好的测试代码应当使用辅助函数减少重复代码:
// 通用的断言辅助函数
func assertIntEqual(t *testing.T, got, want int, name string, args ...interface{}) {
t.Helper() // 标记为辅助函数,错误将定位到调用位置
if got != want {
if len(args) > 0 {
t.Errorf("%s(%v) = %d; 期望 %d", name, args, got, want)
} else {
t.Errorf("%s = %d; 期望 %d", name, got, want)
}
}
}
// 使用辅助函数的测试
func TestWithAssertHelper(t *testing.T) {
t.Run("Add", func(t *testing.T) {
assertIntEqual(t, Add(3, 2), 5, "Add", 3, 2)
})
t.Run("Subtract", func(t *testing.T) {
assertIntEqual(t, Subtract(5, 2), 3, "Subtract", 5, 2)
})
t.Run("Multiply", func(t *testing.T) {
assertIntEqual(t, Multiply(4, 3), 12, "Multiply", 4, 3)
})
}
基准测试(Benchmark)是评估代码性能的测量工具,用于确定代码执行所需的时间和资源。在Go中,基准测试可以帮助我们:
Go的testing
包不仅支持单元测试,还内置了强大的基准测试功能。基准测试函数遵循以下规则:
Benchmark
开头,后跟大写字母开头的单词*testing.B
类型的参数package calculator
import "testing"
func BenchmarkAdd(b *testing.B) {
// 重置计时器(可选)
b.ResetTimer()
// b.N由测试框架动态确定,以获得稳定的测量结果
for i := 0; i < b.N; i++ {
Add(10, 5)
}
}
func BenchmarkMultiply(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
Multiply(10, 5)
}
}
使用go test
命令运行基准测试:
go test -bench=. # 运行所有基准测试
go test -bench=Add # 运行名称匹配"Add"的基准测试
go test -bench=. -benchmem # 同时显示内存分配统计
go test -bench=. -count=5 # 重复测试5次
go test -bench=. -benchtime=10s # 将测试时间延长到10秒
基准测试输出示例:
yanggang@MacBook demo % go test -v ygang.top/demo/calculator -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: ygang.top/demo/calculator
cpu: Apple M1 Pro
BenchmarkAdd
BenchmarkAdd-8 1000000000 0.3163 ns/op 0 B/op 0 allocs/op
BenchmarkMultiply
BenchmarkMultiply-8 1000000000 0.3106 ns/op 0 B/op 0 allocs/op
PASS
ok ygang.top/demo/calculator 0.986s
输出项含义:
BenchmarkAdd-8
:函数名及GOMAXPROCS
设置1000000000
:测试运行了1000000000
次0.3164 ns/op
:每次操作平均耗时0.3164ns
0 B/op
:每次操作分配了0
字节0 allocs/op
:每次操作进行了0
次内存分配性能剖析(Profiling)是分析程序执行期间资源使用情况的技术,帮助开发者识别:
Go提供了强大的内置剖析工具,主要通过runtime/pprof
包和go tool pprof
命令实现。
启用性能剖析的方法有以下几种:
1、测试时启用:通过go test
的命令行参数
go test -cpuprofile=cpu.prof # CPU剖析
go test -memprofile=mem.prof # 内存剖析
go test -blockprofile=block.prof # 阻塞剖析
2、代码中手动启用:通过pprof
包API
package main
import (
"os"
"runtime/pprof"
// ...
)
func main() {
// CPU剖析
cpuFile, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(cpuFile)
defer pprof.StopCPUProfile()
// 执行需要分析的代码
doSomethingIntensive()
// 内存剖析
memFile, _ := os.Create("mem.prof")
defer memFile.Close()
pprof.WriteHeapProfile(memFile)
}
3、Web服务中启用:使用net/http/pprof
包
package main
import (
"net/http"
_ "net/http/pprof" // 仅需导入,不需要显式使用
)
func main() {
// 启动HTTP服务器
http.ListenAndServe(":8080", nil)
}
// 访问 http://localhost:8080/debug/pprof/ 查看剖析数据
pprof
是Go的性能分析工具,可以分析和可视化剖析数据。它提供了多种视图,包括:
假设我们已经生成了CPU剖析文件,现在可以使用pprof
工具分析它:
go tool pprof cpu.prof
# 进入交互式模式后的常用命令:
(pprof) top10 # 显示消耗最多CPU的10个函数
(pprof) list functionName # 显示特定函数的代码和CPU使用情况
(pprof) web # 在浏览器中打开可视化视图(需安装Graphviz)
(pprof) pdf # 生成PDF格式的调用图
(pprof) flame # 生成火焰图(需安装FlameGraph工具)
Web界面示例(需要先安装Graphviz
):
go tool pprof -http=:8080 cpu.prof # 启动Web服务器查看剖析数据
内存分析可以帮助我们找出导致大量内存分配的代码:
go test -memprofile=mem.prof
go tool pprof -alloc_objects mem.prof # 分析对象分配
go tool pprof -alloc_space mem.prof # 分析分配的内存空间
go tool pprof -inuse_objects mem.prof # 分析仍在使用的对象
go tool pprof -inuse_space mem.prof # 分析仍在使用的内存空间
内存泄漏分析,现将两个节点的剖析文件保存
// 在程序的关键点获取内存快照
pprof.WriteHeapProfile(firstFile)
// ...执行操作...
pprof.WriteHeapProfile(secondFile)
然后再比较两个快照找出泄漏
go tool pprof --base firstFile secondFile
对于并发程序,分析goroutine的阻塞和锁等待情况很重要:
// 在代码中启用阻塞剖析
runtime.SetBlockProfileRate(1) // 设置阻塞剖析采样率
// 在代码中启用互斥锁剖析
runtime.SetMutexProfileFraction(1) // 设置互斥锁剖析采样率
启用测试时的阻塞和互斥锁剖析:
go test -blockprofile=block.prof
go test -mutexprofile=mutex.prof
分析剖析数据:
go tool pprof block.prof
go tool pprof mutex.prof