一、单元测试 (Unit Testing)
shell
wrl@DESKTOP-99LNSVB MINGW64 /d/goBackend/src/mainTest/benchmark (master)
$ ls
Benchmark_test.go go.mod main.go split.go
1.1 基本概念
单元测试是对软件中的最小可测试单元进行检查和验证,在Go中通常指函数或方法。
1.2 测试文件命名规则
- 测试文件必须以
_test.go结尾 - 测试文件与被测试文件在同一包内
- 测试函数必须以
Test开头
1.3 基本示例
被测试代码 (math.go):
go
package math
// Add 返回两个整数的和
func Add(a, b int) int {
return a + b
}
// Multiply 返回两个整数的积
func Multiply(a, b int) int {
return a * b
}
测试代码 (math_test.go):
其中参数t用于报告测试失败和附加的日志信息。
go
package math
import "testing"
// TestAdd 测试Add函数
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
// TestMultiply 测试Multiply函数
func TestMultiply(t *testing.T) {
result := Multiply(2, 3)
expected := 6
if result != expected {
t.Errorf("Multiply(2, 3) = %d; want %d", result, expected)
}
}
1.4 表格驱动测试
这是Go中常用的测试模式,可以批量测试多个用例:
go
func TestAddTableDriven(t *testing.T) {
testCases := []struct {
name string
a, b int
expected int
}{
{"正数相加", 2, 3, 5},
{"负数相加", -2, -3, -5},
{"零值相加", 0, 0, 0},
{"正负相加", 5, -3, 2},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := Add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tc.a, tc.b, result, tc.expected)
}
})
}
}
1.5 测试辅助函数
go
func assertEqual(t *testing.T, got, want int) {
t.Helper() // 标记为辅助函数,使错误报告更清晰
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestWithHelper(t *testing.T) {
result := Add(1, 2)
assertEqual(t, result, 3)
}
1.6 testing.T的拥有的方法如下
go
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
测试覆盖率
测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
- Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:
go
wrl@DESKTOP-99LNSVB MINGW64 /d/goBackend/src/mainTest/benchmark (master)
$ go test -cover
PASS
coverage: 100.0% of statements
ok mainTest/benchmark 0.909s
- 生成覆盖率数据文件
go
# 生成覆盖率数据文件
go test -coverprofile=coverage.out
# 输出文件包含:
# 1. 每个文件的执行统计
# 2. 每行代码的执行次数
# mode: 覆盖模式(set, count, atomic)
# 文件名:起始行.起始列,结束行.结束列 语句数 执行次数
mode: set
mainTest/benchmark/main.go:3.14,3.15 0 0
mainTest/benchmark/split.go:5.45,8.13 2 1
mainTest/benchmark/split.go:8.13,12.3 3 1
mainTest/benchmark/split.go:13.2,14.8 2 1
- 查看详细覆盖率报告
go
# 生成HTML格式的覆盖率报告,要先生成coverage.out文件,再使用这个命名,自动会打开html文件
go tool cover -html=coverage.out
# 生成文本报告
$ go tool cover -func=c.out
mainTest/benchmark/main.go:3: main 0.0%
mainTest/benchmark/split.go:5: Split 100.0%
total: (statements) 100.0%
# 按包查看覆盖率
go tool cover -func=coverage.out | grep "your/package"

二、基准测试 (Benchmark Testing)
2.1 基本概念
基准测试用于测量代码的性能,评估函数在不同输入下的执行时间和内存使用情况。
2.2 基准测试规则
- 函数以
Benchmark开头 - 参数为
*testing.B - 使用
b.N作为循环次数
2.3 基本示例
go
// BenchmarkAdd 测试Add函数的性能
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(10, 20)
}
}
// BenchmarkMultiply 测试Multiply函数的性能
func BenchmarkMultiply(b *testing.B) {
for i := 0; i < b.N; i++ {
Multiply(10, 20)
}
}
2.4 带参数的基准测试
go
func BenchmarkAddDifferentInputs(b *testing.B) {
testCases := []struct {
name string
a, b int
}{
{"小数字", 1, 2},
{"中数字", 100, 200},
{"大数字", 10000, 20000},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(tc.a, tc.b)
}
})
}
}
2.5 内存分配基准测试
go
func BenchmarkStringConcatenation(b *testing.B) {
b.ReportAllocs() // 报告内存分配情况
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 100; j++ {
s += "a"
}
}
}
func BenchmarkStringBuilder(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var builder strings.Builder
for j := 0; j < 100; j++ {
builder.WriteString("a")
}
_ = builder.String()
}
}
三、运行测试
3.1 常用命令
bash
# 运行当前目录下所有测试
go test
# 显示详细信息
go test -v
# 运行特定测试函数
go test -v -run TestAdd
# 使用正则表达式匹配测试
go test -v -run "TestAdd.*"
# 运行基准测试
go test -bench=.
# 运行特定基准测试
go test -bench=BenchmarkAdd
# 基准测试并显示内存分配
go test -bench=. -benchmem
# 指定基准测试运行时间
go test -bench=. -benchtime=5s
# 并行运行测试
go test -parallel 4
# 生成覆盖率报告
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
3.2 并行测试
go
func TestParallel(t *testing.T) {
t.Parallel() // 标记为可并行运行
// 测试代码...
}
四、测试辅助工具
4.1 测试Main函数
go
func TestMain(m *testing.M) {
// 测试前的设置
fmt.Println("开始测试...")
// 运行所有测试
code := m.Run()
// 测试后的清理
fmt.Println("测试结束...")
// 退出
os.Exit(code)
}
4.2 子测试
go
package split
import "strings"
// split package with a single split function.
// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
如果测试用例比较多的时候,我们是没办法一眼看出来具体是哪个测试用例失败了。我们可能会想到下面的解决办法:
go
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for name, tc := range tests {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("name:%s expected:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
}
}
}
上面的做法是能够解决问题的。同时Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run执行子测试:
go
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
})
}
}
go
split $ go test -v
=== RUN TestSplit
=== RUN TestSplit/leading_sep
=== RUN TestSplit/simple
=== RUN TestSplit/wrong_sep
=== RUN TestSplit/more_sep
--- FAIL: TestSplit (0.00s)
--- FAIL: TestSplit/leading_sep (0.00s)
split_test.go:83: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
--- PASS: TestSplit/simple (0.00s)
--- PASS: TestSplit/wrong_sep (0.00s)
--- PASS: TestSplit/more_sep (0.00s)
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
我们都知道可以通过-run=RegExp来指定运行的测试用例,还可以通过/来指定要运行的子测试用例,例如:go test -v -run=Split/simple只会运行simple对应的子测试用例。
五、实际示例
5.1 计算器示例
go
// calculator.go
type Calculator struct{}
func (c *Calculator) Add(a, b float64) float64 {
return a + b
}
func (c *Calculator) Subtract(a, b float64) float64 {
return a - b
}
go
// calculator_test.go
func TestCalculator(t *testing.T) {
calc := &Calculator{}
t.Run("加法", func(t *testing.T) {
result := calc.Add(5.5, 2.5)
expected := 8.0
if result != expected {
t.Errorf("Add(5.5, 2.5) = %f; want %f", result, expected)
}
})
t.Run("减法", func(t *testing.T) {
result := calc.Subtract(5.5, 2.5)
expected := 3.0
if result != expected {
t.Errorf("Subtract(5.5, 2.5) = %f; want %f", result, expected)
}
})
}
func BenchmarkCalculatorAdd(b *testing.B) {
calc := &Calculator{}
for i := 0; i < b.N; i++ {
calc.Add(float64(i), float64(i+1))
}
}
六、最佳实践
- 测试命名: 使用清晰的测试名称,描述测试场景
- 表格驱动: 使用表格驱动测试组织多个测试用例
- 避免依赖: 测试不应该依赖外部环境
- 快速执行: 保持测试快速执行
- 独立测试: 每个测试应该独立,不依赖其他测试的状态
- 覆盖率: 追求合理的测试覆盖率,但不是100%
七、常用断言库
虽然Go标准库不提供断言,但社区有多个流行的测试库:
go
// 使用testify库
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithTestify(t *testing.T) {
assert.Equal(t, 4, Add(2, 2), "它们应该相等")
assert.NotNil(t, someValue)
assert.Error(t, err)
}