测试与基准测试
Go 把测试作为一等公民内置在标准库 testing 中。go test ./... 一条命令搞定。
💡命名约定
- 测试文件以
_test.go结尾(与被测文件同目录) - 测试函数:
func TestXxx(t *testing.T),Xxx决定测试名 - 基准测试:
func BenchmarkXxx(b *testing.B) - 示例:
func ExampleXxx()(特殊签名,会出现在 godoc 中)
1. 第一个测试
虽然沙盒里只能跑 main.go,但下面展示的代码就是生产级测试的标准写法。
// math.go
package mathx
func Add(a, b int) int { return a + b }// math_test.go
package mathx
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d, want %d", got, want)
}
}// 沙盒只跑 main.go,我们手动模拟 testing.T 的报告方式
// 真实项目用:go test -v ./...
package main
import "fmt"
func assertEq(name string, got, want any) (passed bool) {
if got != want {
fmt.Printf(" ✗ %s: got %v, want %v\\n", name, got, want)
return false
}
fmt.Printf(" ✓ %s\\n", name)
return true
}
func Add(a, b int) int { return a + b }
func main() {
fmt.Println("TestAdd:")
pass := 0
total := 0
cases := []struct{ name string; a, b, want int }{
{"2+3=5", 2, 3, 5},
{"0+0=0", 0, 0, 0},
{"-1+1=0", -1, 1, 0},
}
for _, c := range cases {
total++
if assertEq(c.name, Add(c.a, c.b), c.want) {
pass++
}
}
fmt.Printf("结果:%d/%d\\n", pass, total)
}ℹ️沙盒不能跑 go test
go test 需要 _test.go 文件。沙盒只运行 main.go。所以这里的 Playground 用 main() 模拟测试报告的格式;真实开发中请用 go test -v。
2. 表格驱动测试(Table-Driven Test)
把测试用例写成切片,循环跑。Go 风格最常见的测试模式。
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"正数", 2, 3, 5},
{"零", 0, 0, 0},
{"负数", -1, 1, 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}
}package main
import (
"fmt"
"strings"
)
// Split 按 sep 切分 s,限制最多 n 段(n=0 不限制)
func Split(s, sep string, n int) []string {
if n == 0 {
return strings.Split(s, sep)
}
return strings.SplitN(s, sep, n)
}
type tc struct {
name string
s, sep string
n int
wantLen int
wantFirst string
wantLast string
}
func main() {
tests := []tc{
{"基本", "a,b,c", ",", 0, 3, "a", "c"},
{"限制", "a,b,c", ",", 2, 2, "a", "b"},
{"不分割", "abc", ",", 0, 1, "abc", "abc"},
}
fail := 0
for _, c := range tests {
got := Split(c.s, c.sep, c.n)
ok := len(got) == c.wantLen && got[0] == c.wantFirst && got[len(got)-1] == c.wantLast
if ok {
fmt.Printf(" ✓ %s -> %v\\n", c.name, got)
} else {
fail++
fmt.Printf(" ✗ %s: got %v\\n", c.name, got)
}
}
fmt.Printf("\\n通过 %d/%d\\n", len(tests)-fail, len(tests))
}💡表格驱动的好处
- 加新用例:只追加一行
- 一目了然看到所有边界条件
- 子测试名清晰(
t.Run会显示成TestSplit/基本)
3. 子测试 t.Run
子测试让你只跑某一组:-run TestSplit/限制。
func TestGroup(t *testing.T) {
t.Run("A", func(t *testing.T) { ... })
t.Run("B", func(t *testing.T) { ... })
}package main
import "fmt"
func reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// 模拟 t.Run 的报告
func sub(name string, fn func() bool) {
if fn() {
fmt.Printf(" --- PASS: %s\\n", name)
} else {
fmt.Printf(" --- FAIL: %s\\n", name)
}
}
func main() {
fmt.Println("TestReverse:")
sub("ASCII", func() bool { return reverse("hello") == "olleh" })
sub("中文", func() bool { return reverse("你好Go") == "oG好你" })
sub("空串", func() bool { return reverse("") == "" })
sub("单字符", func() bool { return reverse("a") == "a" })
}4. 基准测试 Benchmark
基准测试用 b.N 控制循环次数,自动调整直到稳定。
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// 运行:go test -bench=.// 沙盒里我们用 time 测量,模拟 Benchmark 的报告方式
package main
import (
"fmt"
"strings"
"time"
)
func concatLoop(n int) string {
var s string
for i := 0; i < n; i++ {
s += "a"
}
return s
}
func concatBuilder(n int) string {
var b strings.Builder
b.Grow(n)
for i := 0; i < n; i++ {
b.WriteByte('a')
}
return b.String()
}
func runBench(name string, fn func()) {
const runs = 5
var total time.Duration
for i := 0; i < runs; i++ {
start := time.Now()
fn()
total += time.Since(start)
}
avg := total / runs
fmt.Printf("Benchmark%-20s %8d ns/op\\n", name, avg.Nanoseconds())
}
func main() {
const N = 10000
runBench("ConcatLoop", func() { _ = concatLoop(N) })
runBench("ConcatBuilder", func() { _ = concatBuilder(N) })
}ℹ️输出会随机器波动
time 在不同机器、不同时刻的测量值会不同。这里展示的是"哪个更快"——strings.Builder 远胜 +=。
5. Example 测试(出现在 godoc 中)
ExampleXxx 有特殊价值:它的输出会显示在 pkg.go.dev 的文档里。
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}💡为什么用 Example
- 文档即测试,保证示例代码永远能跑
- godoc 自动检测
// Output:注释作为预期 - 比纯文字注释更可靠
6. 完整示例:测试一个 Stack
package main
import "fmt"
type Stack[T any] struct{ data []T }
func (s *Stack[T]) Push(v T) { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() (T, bool) {
n := len(s.data)
if n == 0 { var z T; return z, false }
v := s.data[n-1]
s.data = s.data[:n-1]
return v, true
}
func runCase(name string, ok bool) {
mark := "✓"
if !ok { mark = "✗" }
fmt.Printf(" %s %s\\n", mark, name)
}
func main() {
fmt.Println("TestStack:")
// 1. Push 后能 Pop
s := &Stack[int]{}
s.Push(1); s.Push(2); s.Push(3)
v, _ := s.Pop()
runCase("Pop 返回栈顶 3", v == 3)
v, _ = s.Pop()
runCase("Pop 接着返回 2", v == 2)
v, _ = s.Pop()
runCase("Pop 接着返回 1", v == 1)
_, ok := s.Pop()
runCase("空栈 Pop 返回 ok=false", ok == false)
runCase("空栈 Pop 后 len=0", s.data == nil || len(s.data) == 0)
}⚠️测试约定速查
t.Errorf:报告错误,继续跑t.Fatalf:报告错误,立即终止当前测试t.Logf:打印调试信息(只在-v显示)t.Helper():标记当前函数是辅助函数,错误显示调用方行号
🎯 练习
// 任务:写表格驱动测试验证 IsPalindrome
// 1. 至少 5 个 case:单字符、偶数回文、奇数回文、非回文、空串、中文
// 2. 模拟 go test 的报告格式
// 3. 不通过的 case 用 ✗ 标记
package main
import (
"fmt"
"strings"
"unicode"
)
// IsPalindrome 判断字符串是否是回文(忽略大小写、空格)
func IsPalindrome(s string) bool {
var b strings.Builder
for _, r := range s {
if !unicode.IsSpace(r) {
b.WriteRune(unicode.ToLower(r))
}
}
t := b.String()
for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 {
if t[i] != t[j] {
return false
}
}
return true
}
func main() {
tests := []struct {
name string
s string
want bool
}{
{"单字符", "a", true},
{"偶数回文", "abba", true},
{"奇数回文", "level", true},
{"非回文", "hello", false},
{"空串", "", true},
{"中文", "上海自来水来自海上", true},
{"带空格", "A man a plan a canal Panama", true},
}
pass := 0
for _, c := range tests {
got := IsPalindrome(c.s)
if got == c.want {
fmt.Printf(" ✓ %s\\n", c.name)
pass++
} else {
fmt.Printf(" ✗ %s: got %v, want %v\\n", c.name, got, c.want)
}
}
fmt.Printf("\\n通过 %d/%d\\n", pass, len(tests))
}小结
- ✅ Go 测试是
testing标准库 +go test命令 - ✅ 文件名
_test.go、函数名TestXxx/BenchmarkXxx/ExampleXxx - ✅ 表格驱动 = 测试用例切片 + 循环断言
- ✅
t.Run创建子测试,支持-run TestXxx/子名 - ✅ Benchmark 用
b.N自动调整循环次数 - ✅ Example 既能跑测试,又能出现在 godoc
- ✅ 沙盒里用
main()模拟测试,真实项目用go test -v - ✅
t.Helper()标记辅助函数,错误指向真实调用方
下一章是 项目实战。