下载工作台
Go 编程实战

testing 与 benchmark

试读上半部分 · 解锁后可读全文

第 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/logparserJSON 行、坏行、空文件
报表 CSVinternal/report表头、行数、字段值
Top Ninternal/sortalgo边界 n=0、降序(ch14)
HTTPinternal/httpxhealth 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失败继续执行

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。