前言

“没测试的代码 = 不知道能不能跑的代码”。Go 把测试做成语言级公民:go test 开箱即用,没有选择困难。这篇从规范到进阶一次讲清。

一、最小测试

规则:测试文件以 _test.go 结尾,函数签名 func TestXxx(t *testing.T)。

// split.go
package strutil

import "strings"

func Split(s, sep string) []string {
    return strings.Split(s, sep)
}
// split_test.go
package strutil

import "testing"

func TestSplit(t *testing.T) {
    got := Split("a,b,c", ",")
    want := []string{"a", "b", "c"}

    if len(got) != len(want) {
        t.Errorf("长度不符: got %d, want %d", len(got), len(want))
    }
    for i := range want {
        if got[i] != want[i] {
            t.Errorf("第%d个元素: got %q, want %q", i, got[i], want[i])
        }
    }
}
go test ./...            # 跑当前模块全部测试
go test -v               # 显示每个用例
go test -run TestSplit   # 只跑匹配的

二、表驱动测试(Go 的招牌写法)

一组输入输出写成表,循环断言——新增用例只加一行:

func TestSplit(t *testing.T) {
    tests := []struct {
        name  string
        input string
        sep   string
        want  []string
    }{
        {"普通逗号", "a,b,c", ",", []string{"a", "b", "c"}},
        {"无分隔符", "abc", ",", []string{"abc"}},
        {"空字符串", "", ",", []string{""}},
        {"分隔符在两端", ",a,", ",", []string{"", "a", ""}},
        {"中文", "你好,世界", ",", []string{"你好", "世界"}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {      // 子测试: 单独报告、单独运行
            got := Split(tt.input, tt.sep)
            if !slices.Equal(got, tt.want) {
                t.Errorf("Split(%q, %q) = %v, want %v",
                    tt.input, tt.sep, got, tt.want)
            }
        })
    }
}
go test -v -run TestSplit/中文     # 只跑某个子用例

三、testify:让断言好看点

go get github.com/stretchr/testify
import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestSplit(t *testing.T) {
    got := Split("a,b,c", ",")

    assert.Equal(t, []string{"a", "b", "c"}, got)   // 失败继续跑完
    require.Len(t, got, 3, "应切出3段")               // 失败立即终止(防后续 panic)
    assert.Contains(t, got, "b")
    assert.NoError(t, err)
}

assert(失败继续)vs require(失败即停):后者用于"后面依赖前面结果"的场景。

四、覆盖率

go test -cover ./...
# ok   demo/strutil  0.012s  coverage: 85.7% of statements

# 生成 HTML 报告, 看哪些行没测到
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out -o cover.html

# CI 里卡阈值(低于80%失败)
go test -cover ./... | awk -F'coverage: ' '/coverage/ {gsub("%.*","",$2); if ($2+0 < 80) exit 1}'

覆盖率是体检指标不是 KPI:80% 且关键路径全测 远胜于刷到 95% 的无脑断言。

五、Benchmark 基准测试

func BenchmarkXxx(b *testing.B),框架自动调 b.N 次直到测量稳定:

func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {        // 循环体必须是 b.N 次
        Split("a,b,c,d,e,f,g,h", ",")
    }
}
go test -bench=. -benchmem ./...
# BenchmarkSplit-8   5824932   204.1 ns/op   112 B/op   1 allocs/op
#                    │          │            │          └ 每次操作堆分配次数
#                    │          │            └ 每次操作分配字节数
#                    │          └ 单次操作耗时
#                    └ -8 = GOMAXPROCS

优化对比实战:

// 优化前: 每次都分配
func Join(b []byte, s string) string { return string(b) + s }

// 优化后: 预分配
func Join2(b []byte, s string) string {
    out := make([]byte, 0, len(b)+len(s))
    out = append(out, b...)
    out = append(out, s...)
    return string(out)
}

func BenchmarkJoin(b *testing.B)  { for i := 0; i < b.N; i++ { Join(x, "y") } }
func BenchmarkJoin2(b *testing.B) { for i := 0; i < b.N; i++ { Join2(x, "y") } }

// BenchmarkJoin-8    8ns/op    16B/op  1 allocs/op
# BenchmarkJoin2-8    5ns/op     0B/op  0 allocs/op  ← 预分配立功

六、并行测试与超时

func TestParallel(t *testing.T) {
    tests := loadCases()                     // 100 个用例
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()                     // 子用例并行跑(大幅提速)
            got := heavy(tt.input)
            assert.Equal(t, tt.want, got)
        })
    }
}
go test -timeout 60s ./...      # 全局超时(默认10min, CI必配)
go test -race ./...             # 竞态检测(测试并发代码必开!)

七、测外部依赖:接口 + mock

依赖外部服务时,先抽象成接口(结构体接口篇的招数),测试时注入假实现:

type Notifier interface {
    Notify(title, body string) error
}

type fakeNotifier struct {
    calls int
    err   error
}
func (f *fakeNotifier) Notify(t, b string) error {
    f.calls++
    return f.err
}

func TestAlerter(t *testing.T) {
    fn := &fakeNotifier{err: errors.New("network down")}
    a := NewAlerter(fn)

    err := a.Fire("cpu high")
    require.Error(t, err)             // 通知失败要有正确行为
    assert.Equal(t, 1, fn.calls)      // 且真的调用过一次
}

gomock/testify mock 是这套思路的自动化版本——但能手写 fake 时优先手写,可读性更好。

八、CI 集成(GitLab CI 示例)

test:
  stage: test
  image: golang:1.22
  script:
    - go vet ./...                      # 静态检查
    - go test -race -coverprofile=cover.out -timeout 120s ./...
    - go tool cover -func=cover.out | tail -1
  coverage: '/coverage: \d+\.\d+/'

九、常见坑

现象 原因
no required module provides package 测试文件 package 声明错了;或没 go mod init
benchmark 数值乱跳 机器忙/电源管理;多跑几次取稳定值,加 -benchtime=3s
测试顺序依赖互相污染 用例要幂等;共享状态用 setup/teardown 隔离
t.Parallel 后 data race 闭包变量被并发改;循环变量传参(老版本 Go)
mock 之后测了个寂寞 假实现直接返回"想要的"——mock 测的是交互,真集成要 e2e 补

小结

需求 工具
基本测试 TestXxx(t *testing.T)
多用例 表驱动 + t.Run 子测试
断言 testify assert/require
覆盖率 -coverprofile + HTML
性能 -bench + -benchmem
并发正确性 -race
外部依赖 接口 + 手写 fake

本文是「Golang」系列第 9 篇。