第 13 章 · testing 表驱动、benchmark 与 -race
本章目标:编写 表驱动测试(table-driven tests);使用 httptest 测 HTTP handler;运行 go test ./...、-cover 覆盖率、-race 竞态检测;编写 benchmark 评估解析与排序性能;为 toolkit-go ch15 验收「单元测试 ≥3」做准备;对照 java-dev ch12 JUnit。
学时建议:3~4 小时(含 2 小时跟练)
前置:完成 go-dev ch12(net/http)。
13.1 场景说明:质量门禁
ch15 毕业验收要求 ≥3 个单元测试 且 go test ./... 通过。应覆盖:
| 模块 | 包路径 | 测试重点 |
|---|---|---|
| 订单计算 | internal/calc | 金额、负价格、空列表 |
| 日志解析 | internal/logparser | JSON 行、坏行、空文件 |
| 报表 CSV | internal/report | 表头、行数、字段值 |
| Top N | internal/sortalgo | 边界 n=0、降序(ch14) |
| HTTP | internal/httpx | health 200(ch12) |
go test ./... # 全量
go test -race ./... # 竞态(ch09 并发必跑)
go test -cover ./... # 覆盖率
go test -bench=. ./... # 性能
测试哲学:Go 社区偏好 表驱动 — 一个 TestXxx 内多组 {name, input, want},新增用例只加表格行。
13.2 表驱动测试基础
// internal/calc/order_test.go
package calc
import "testing"
func TestCalcLinesTotal(t *testing.T) {
tests := []struct {
name string
lines []LineItem
want int64
wantErr bool
}{
{
name: "single line",
lines: []LineItem{
{Product: Product{Price: 6800}, Quantity: 2},
},
want: 13600,
},
{
name: "multiple lines",
lines: []LineItem{
{Product: Product{Price: 1000}, Quantity: 1},
{Product: Product{Price: 2000}, Quantity: 3},
},
want: 7000,
},
{
name: "empty cart",
lines: nil,
wantErr: true,
},
{
name: "negative price rejected",
lines: []LineItem{
{Product: Product{Price: -1}, Quantity: 1},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CalcLinesTotal(tt.lines)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("got %d want %d", got, tt.want)
}
})
}
}
| 约定 | 说明 |
|---|---|
| 文件命名 | *_test.go |
| 函数命名 | TestXxx(t *testing.T) |
| 子测试 | t.Run(name, func(t *testing.T){}) |
| 包名 | 同包白盒 calc;黑盒 calc_test + 只测导出 API |
13.3 logparser 表驱动测试
// internal/logparser/parser_test.go
package logparser
import (
"testing"
)
func TestParseLine(t *testing.T) {
tests := []struct {
name string
line string
wantPath string
wantMs int
wantErr bool
}{
{
name: "valid line",
line: `{"time":"2026-08-19T10:00:01Z","method":"GET","path":"/api/ping","status":200,"duration_ms":45,"request_id":"r1"}`,
wantPath: "/api/ping",
wantMs: 45,
},
{
name: "invalid json",
line: `{broken`,
wantErr: true,
},
{
name: "empty line",
line: ``,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e, err := ParseLine(tt.line)
if tt.wantErr {
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatal(err)
}
if e.Path != tt.wantPath || e.DurationMs != tt.wantMs {
t.Errorf("got %+v", e)
}
})
}
}
func TestParseAll_Skipped(t *testing.T) {
lines := []string{
`{"time":"2026-08-19T10:00:01Z","path":"/a","duration_ms":10,"status":200}`,
`{bad}`,
`{"time":"2026-08-19T10:00:02Z","path":"/b","duration_ms":20,"status":200}`,
}
entries, st := ParseAll(lines)
if len(entries) != 2 {
t.Fatalf("parsed %d want 2", len(entries))
}
if st.Skipped != 1 {
t.Errorf("skipped %d want 1", st.Skipped)
}
}
13.4 httptest 测 HTTP handler
无需启动真实端口:
// internal/httpx/handlers_test.go
package httpx
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d want 200", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); ct == "" {
t.Error("missing content-type")
}
var body map[string]string
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["status"] != "ok" {
t.Errorf("body %v", body)
}
}
gin-web ch14 将用 httptest + gin.Engine 做集成测试,模式相同。
13.5 testify 断言(选修)
ch08 已 go get github.com/stretchr/testify:
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestProductPublish(t *testing.T) {
p, err := NewProduct("go-book", "Go 书", 6800)
require.NoError(t, err)
p.Publish()
require.True(t, p.IsPublished)
require.Equal(t, int64(6800), p.Price)
}
| 包 | 用途 |
|---|---|
require | 失败立即 t.Fatal |
assert | 失败继续执行 |