测试

go test 是 Go 内置的测试工具链:测试代码与源码同目录存放(*_test.go),无需第三方框架即可覆盖单元、接口与基准测试。没有测试的生产代码不应合入主干,这是 Go 团队的工程共识。

测试基础

测试函数以 Test 开头、参数为 *testing.T,放在同包的 _test.go 文件中:

package money

func AddCents(a, b int) int {
    return a + b
}
package money

import "testing"

func TestAddCents(t *testing.T) {
    got := AddCents(100, 250)
    if got != 350 {
        t.Fatalf("AddCents(100, 250) = %d, want 350", got)
    }
}
go test ./...            # 运行全部测试
go test -run TestAddCents -v   # 运行单个测试并输出详情
go test -count=1 ./...   # 忽略缓存强制重跑

表驱动测试:Go 的标志性格式

同一逻辑覆盖多组输入输出时,使用"用例切片 + 循环断言"的表驱动格式:

package slug

import "testing"

func TestSlugify(t *testing.T) {
    tests := []struct {
        name string // 子测试名
        in   string
        want string
    }{
        {"中文与空格", "Go 语言 教程", "go-yu-yan-jiao-cheng"},
        {"连续分隔符", "go--lang", "go-lang"},
        {"空字符串", "", ""},
        {"大写转小写", "Hello World", "hello-world"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) { // 每个用例一个子测试
            t.Parallel()                    // 用例间并行执行
            got := Slugify(tt.in)
            if got != tt.want {
                t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
            }
        })
    }
}
Tip

表驱动测试的价值:

  • 新增用例只需加一行数据,断言逻辑零重复。
  • -run TestSlugify/中文 可单独运行某个子测试。
  • 失败输出精确到用例名,定位成本极低。

断言选择:t.Fatalf 用于后续依赖该步骤的场景(失败即终止);普通断言用 t.Errorf 让所有用例跑完。

接口测试:httptest

net/http/httptest 提供两个层级的测试工具:ResponseRecorder 内存化单次请求,NewServer 启动真实监听。

package api

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    _ = json.NewEncoder(w).Encode(map[string]string{"msg": "hello"})
}

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/hello", nil)
    rec := httptest.NewRecorder()

    helloHandler(rec, req) // 直接调用,无需真实网络

    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want 200", rec.Code)
    }

    var body map[string]string
    if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
        t.Fatalf("decode body: %v", err)
    }
    if body["msg"] != "hello" {
        t.Errorf("msg = %q, want hello", body["msg"])
    }
}

需要测试完整链路(路由、中间件、序列化)时,用 httptest.NewServer 启动真实服务:

func TestServerEndToEnd(t *testing.T) {
    srv := httptest.NewServer(BuildMux()) // BuildMux 返回 http.Handler
    t.Cleanup(srv.Close)                  // 测试结束自动关闭

    resp, err := http.Get(srv.URL + "/hello")
    if err != nil {
        t.Fatalf("request: %v", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Fatalf("status = %d", resp.StatusCode)
    }
}

依赖替换:小接口与替身

生产代码依赖外部服务(数据库、第三方 API)时,测试必须隔离它们。Go 的做法是定义小接口,注入假实现,而非重度 mock 框架:

// 面向接口设计:业务只依赖抽象
type Store interface {
    Save(ctx context.Context, code, url string) error
    Load(ctx context.Context, code string) (string, error)
}

type Service struct {
    store Store
}

func (s *Service) Resolve(ctx context.Context, code string) (string, error) {
    return s.store.Load(ctx, code)
}
// 内存假实现(fake),快且无外部依赖
type fakeStore struct {
    data map[string]string
}

func (f *fakeStore) Save(_ context.Context, code, url string) error {
    f.data[code] = url
    return nil
}

func (f *fakeStore) Load(_ context.Context, code string) (string, error) {
    url, ok := f.data[code]
    if !ok {
        return "", errors.New("not found")
    }
    return url, nil
}

func TestResolve(t *testing.T) {
    svc := &Service{store: &fakeStore{data: map[string]string{"abc": "https://go.dev"}}}

    got, err := svc.Resolve(context.Background(), "abc")
    if err != nil || got != "https://go.dev" {
        t.Fatalf("Resolve() = %q, %v", got, err)
    }
}

:::tip 替身选择经验:

  • Fake(内存实现):默认选择,还可顺带验证业务逻辑。
  • Stub:固定返回值,用于覆盖特定分支。
  • 接口方法很多时再考虑 gomockcounterfeiter 等代码生成工具;小接口手写 fake 通常更快。
  • 测试第三方库提供的接口时优先使用其官方测试工具(如 sqlmock)。 :::

基准测试与竞态检测

func BenchmarkSlugify(b *testing.B) {
    for b.Loop() { // Go 1.24+;旧版本用 for i := 0; i < b.N; i++
        _ = Slugify("Go 语言 教程")
    }
}
go test -bench . -benchmem        # 运行基准并输出内存分配
go test -race ./...               # 竞态检测,CI 必开
go test -cover -coverprofile=c.out ./... && go tool cover -html=c.out

:::warning 三条铁律:

  1. 测试文件与被测代码同包(白盒)或同目录外包 package xxx_test(黑盒);对外 API 优先黑盒测试。
  2. 并发代码必须在 -race 下通过,否则视为测试未通过。
  3. 测试要快速且相互独立:不依赖执行顺序、不依赖外部网络,需要真实依赖时用 Docker 或 testcontainers。 :::

小结

  • 表驱动 + 子测试是 Go 单元测试的标准格式。
  • httptest 覆盖 handler 单测与真实监听两级接口测试。
  • 依赖注入小接口 + 手写 fake,比 mock 框架更符合 Go 风格。
  • CI 中永远开启 -race,基准测试用 b.Loop/b.N 编写。