测试与工程实践
testing 包、表驱动、基准、httptest、Fuzz、工具链
单元测试
测试文件以 _test.go 结尾,跟被测代码同目录;函数签名 func TestXxx(t *testing.T),名字必须以 Test 开头且后接大写字母。go 命令会自动找到它们。
// add.go
package math
func Add(a, b int) int { return a + b }// add_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
if got != 5 {
t.Errorf("Add(2,3) = %d, want 5", got)
}
}$ go test ./...
$ go test -run TestAdd -v
$ go test -count=1 ./... # 关闭测试结果缓存表驱动测试 + 子测试
Go 最常见的测试模式:把用例放进切片,用 t.Run 让每个用例成为独立子测试。失败时能看到具体哪个子用例挂了,也支持 -run 单独跑一条。
// add_test.go
package math
import "testing"
func TestAdd_Table(t *testing.T) {
tests := []struct {
name string
a, b, want int
}{
{"both zero", 0, 0, 0},
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
}
for _, tc := range tests {
tc := tc // Go < 1.22 必须重新绑定
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // 子测试并行
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}
}测试辅助:Helper / Cleanup / TempDir
testing.T 提供一组用于隐藏底层细节、自动清理资源、避免污染的工具。
// db_test.go
package storage
import (
"path/filepath"
"testing"
)
type DB struct{ path string }
func Open(path string) (*DB, error) { return &DB{path: path}, nil }
func (d *DB) Close() error { return nil }
func mustOpenDB(t *testing.T) *DB {
t.Helper() // 失败时定位到调用方而不是这一行
dir := t.TempDir() // 测试结束自动删
db, err := Open(filepath.Join(dir, "a.db"))
if err != nil {
t.Fatalf("open: %v", err)
}
t.Cleanup(func() { db.Close() }) // 比 defer 更可靠
return db
}
func TestDBOpen(t *testing.T) {
db := mustOpenDB(t)
_ = db
}并行测试与 -race
t.Parallel() 让用例并发执行——CPU 多就能更快;同时建议加 -race 让 race detector 把并发 bug 提前暴露。
$ go test -race -parallel=8 ./...testdata 目录
go 工具链约定:名为 testdata 的目录会被忽略(不会触发编译、不会被 vet)。把测试用的 JSON / 黄金文件放进去最合适。
// render_test.go
package render
import (
"bytes"
"flag"
"os"
"testing"
)
var update = flag.Bool("update", false, "rewrite golden files")
func Render(in []byte) []byte { return bytes.ToUpper(in) }
func TestRender(t *testing.T) {
in, err := os.ReadFile("testdata/input.md")
if err != nil {
t.Fatal(err)
}
want, _ := os.ReadFile("testdata/expected.html")
got := Render(in)
if !bytes.Equal(got, want) {
if *update { // 用 -update 重写黄金文件
os.WriteFile("testdata/expected.html", got, 0o644)
return
}
t.Errorf("got:\n%s\nwant:\n%s", got, want)
}
}httptest:测试 HTTP handler
net/http/httptest 提供假的 Server 和 ResponseRecorder:服务端、客户端测试都不需要真起端口。
// api_test.go
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
type User struct{ ID int }
func UserHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "user:", r.PathValue("id"))
}
func Fetch(url string) (User, error) {
resp, err := http.Get(url)
if err != nil {
return User{}, err
}
defer resp.Body.Close()
var u User
err = json.NewDecoder(resp.Body).Decode(&u)
return u, err
}
// 测 handler
func TestUserHandler(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", UserHandler)
req := httptest.NewRequest("GET", "/users/42", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("status = %d", rec.Code)
}
}
// 测客户端代码(要打到的 server 是假的)
func TestFetch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"ID":1}`)
}))
defer srv.Close()
got, err := Fetch(srv.URL)
if err != nil || got.ID != 1 {
t.Fatalf("Fetch = %+v, %v", got, err)
}
}Example 测试:双重作用
ExampleXxx 函数既是文档(在 pkg.go.dev 自动展示),又是测试——go test 会比较 // Output: 注释和实际输出。
// example_test.go
package math
import "fmt"
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}基准测试
BenchmarkXxx 函数接收 *testing.B;循环 b.N 次,Go 自动调整 N 直到测量稳定。-benchmem 输出每次操作的内存分配次数和字节数。
// bench_test.go
package math
import (
"strings"
"testing"
)
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(1, 2)
}
}
func BenchmarkJoin(b *testing.B) {
parts := []string{"a", "b", "c"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = strings.Join(parts, "-")
}
}$ go test -bench=. -benchmem -benchtime=2s
$ go test -bench=Add -cpuprofile=cpu.out
$ go tool pprof cpu.out # 进交互式分析Fuzz 测试(Go 1.18+)
FuzzXxx 函数接收 *testing.F,先用 Add 注册种子,再用 Fuzz(func(t, ...))) 让 Go 自动变异输入找崩溃点和非预期 panic。特别适合解析器、协议层。
// fuzz_test.go
package math
import (
"strconv"
"testing"
)
func FuzzParseInt(f *testing.F) {
f.Add("42")
f.Add("-1")
f.Add("")
f.Fuzz(func(t *testing.T, s string) {
v, err := strconv.Atoi(s)
if err == nil {
if got := strconv.Itoa(v); got != s {
t.Errorf("round trip: %q -> %d -> %q", s, v, got)
}
}
})
}$ go test -fuzz=FuzzParseInt -fuzztime=30s覆盖率
$ go test -cover ./...
$ go test -coverprofile=cov.out && go tool cover -html=cov.out
$ go test -coverpkg=./... ./... # 含上跨包调用Mock 与依赖注入
Go 推荐通过接口注入依赖,测试时传一个简单的 stub struct,不需要 mock 框架。如果接口方法多,可以用 gomock / mockery 生成。
// notify_test.go
package app
import "testing"
type User struct{ Email string }
type Mailer interface {
Send(to, body string) error
}
type service struct{ mail Mailer }
func (s *service) Notify(u User) error {
return s.mail.Send(u.Email, "hello")
}
// 测试用 stub
type fakeMail struct{ sent []string }
func (f *fakeMail) Send(to, body string) error {
f.sent = append(f.sent, to)
return nil
}
func TestNotify(t *testing.T) {
m := &fakeMail{}
s := &service{mail: m}
s.Notify(User{Email: "a@b.c"})
if len(m.sent) != 1 {
t.Fatal("not sent")
}
}工程实践工具链
- gofmt / goimports:唯一的格式化标准,编辑器保存自动跑
- go vet:内建静态检查
- staticcheck / golangci-lint:聚合多个 linter,CI 标配
- go build -ldflags "-s -w -X main.version=$(git describe)":瘦身 + 注入版本
- go install 把命令装到 $GOBIN;go run 直接跑 main 包
- 交叉编译:GOOS=linux GOARCH=amd64 go build -o app
- go generate:触发代码生成(stringer / mockgen / sqlc 等)
- go tool pprof / trace:性能剖析与调度可视化