golang-testing

golang-testing

热门

生产级 Go 测试——表格驱动测试、testify 套件与模拟、并行测试、模糊测试、测试夹具、基于 goleak 的协程泄漏检测、快照测试、代码覆盖率、集成测试、惯用测试命名。在编写或审查 Go 测试、选择测试方法、配置 Go 测试 CI、或调试不稳定/慢速测试时使用。有关 testify 特定 API,请参见 `samber/cc-skills-golang@golang-stretchr-testify`;有关测量方法,请参见 `samber/cc-skills-golang@golang-benchmark`。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-testing
描述

生产级 Go 测试——表格驱动测试、testify 套件与模拟、并行测试、模糊测试、测试夹具、基于 goleak 的协程泄漏检测、快照测试、代码覆盖率、集成测试、惯用测试命名。在编写或审查 Go 测试、选择测试方法、配置 Go 测试 CI、或调试不稳定/慢速测试时使用。有关 testify 特定 API,请参见 `samber/cc-skills-golang@golang-stretchr-testify`;有关测量方法,请参见 `samber/cc-skills-golang@golang-benchmark`。

角色: 你是一位将测试视为可执行规范的 Go 工程师。你编写测试是为了约束行为,而不是为了达到覆盖率目标。

思考模式: 使用 ultrathink 进行测试策略设计和失败分析。浅层推理会遗漏边界情况,产生今天通过但明天就失效的脆弱测试。

模式:

  • 编写模式 — 为现有或新代码生成新测试。按顺序处理被测代码;使用 gotests 搭建表格驱动测试框架,然后补充边界情况和错误路径。
  • 审查模式 — 审查 PR 中的测试变更。关注差异:检查新行为的覆盖率、断言质量、表格驱动结构以及是否缺乏不稳定模式。按顺序进行。
  • 审计模式 — 审计现有测试套件中的缺口、不稳定或不良模式(顺序依赖测试、缺少 t.Parallel()、实现细节耦合)。按关注点启动最多 3 个并行子代理:(1) 单元测试质量和覆盖率缺口,(2) 集成测试隔离和构建标签,(3) 协程泄漏和竞态条件。
  • 调试模式 — 测试失败或不稳定。按顺序工作:可靠地复现,隔离失败的断言,在生产代码或测试设置中追溯根本原因。

社区默认。 明确覆盖 samber/cc-skills-golang@golang-testing 技能的公司技能优先。

依赖:

  • gotests: go install github.com/cweill/gotests/gotests@latest

Go 测试最佳实践

本技能指导为 Go 应用程序创建生产级测试。遵循以下原则编写可维护、快速且可靠的测试。

最佳实践总结

  1. 表格驱动测试必须使用命名子测试——每个测试用例需要一个传递给 t.Runname 字段
  2. 集成测试必须使用构建标签(//go:build integration)与单元测试分离
  3. 测试不得依赖执行顺序——每个测试必须可独立运行
  4. 独立的测试应尽可能使用 t.Parallel()
  5. 绝不测试实现细节——测试可观察行为和公共 API 契约
  6. 包含协程的包应在 TestMain 中使用 goleak.VerifyTestMain 检测协程泄漏
  7. 将 testify 作为辅助工具,而非标准库的替代品
  8. 模拟接口,而非具体类型
  9. 保持单元测试快速(< 1ms),使用构建标签进行集成测试
  10. 在 CI 中启用竞态检测运行测试
  11. 包含示例作为可执行文档

测试结构与组织

文件约定

// package_test.go - 同一包内的测试(白盒,可访问未导出内容)
package mypackage

// mypackage_test.go - 测试包内的测试(黑盒,仅公共 API)
package mypackage_test

命名约定

func TestAdd(t *testing.T) { ... }               // 函数测试
func TestMyStruct_MyMethod(t *testing.T) { ... } // 方法测试
func BenchmarkAdd(b *testing.B) { ... }          // 基准测试
func ExampleAdd() { ... }                        // 示例
func FuzzAdd(f *testing.F) { ... }               // 模糊测试

表格驱动测试

表格驱动测试是 Go 中测试多种场景的惯用方式。始终为每个测试用例命名。

func TestCalculatePrice(t *testing.T) {
    tests := []struct {
        name     string
        quantity int
        unitPrice float64
        expected  float64
    }{
        {
            name:      "single item",
            quantity:  1,
            unitPrice: 10.0,
            expected:  10.0,
        },
        {
            name:      "bulk discount - 100 items",
            quantity:  100,
            unitPrice: 10.0,
            expected:  900.0, // 10% discount
        },
        {
            name:      "zero quantity",
            quantity:  0,
            unitPrice: 10.0,
            expected:  0.0,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := CalculatePrice(tt.quantity, tt.unitPrice)
            if got != tt.expected {
                t.Errorf("CalculatePrice(%d, %.2f) = %.2f, want %.2f",
                    tt.quantity, tt.unitPrice, got, tt.expected)
            }
        })
    }
}

单元测试

单元测试应快速(< 1ms)、隔离(无外部依赖)且确定性。

测试 HTTP 处理器

使用 httptest 进行处理器测试,采用表格驱动模式。有关请求/响应体、查询参数、头部和状态码断言的示例,请参见 HTTP 测试

使用 goleak 检测协程泄漏

使用 go.uber.org/goleak 检测泄漏的协程,尤其适用于并发代码:

import (
    "testing"
    "go.uber.org/goleak"
)

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

排除特定协程栈(针对已知泄漏或库协程):

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m,
        goleak.IgnoreCurrent(),
    )
}

或按测试排除:

func TestWorkerPool(t *testing.T) {
    defer goleak.VerifyNone(t)
    // ... 测试代码 ...
}

testing/synctest 用于确定性协程测试

testing/synctest(Go 1.25+)为协程、定时器、截止时间和上下文取消提供确定性测试。时间仅在所有协程阻塞时前进,使顺序可预测。

何时使用 synctest 而非真实时间:

  • 测试包含基于时间操作(time.Sleep, time.After, time.Ticker)的并发代码
  • 当竞态条件需要可复现时
  • 当测试因时序问题而不稳定时
import (
    "context"
    "testing"
    "testing/synctest"
    "time"
)

func TestContextTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        const timeout = 5 * time.Second

        ctx, cancel := context.WithTimeout(t.Context(), timeout)
        defer cancel()

        time.Sleep(timeout - time.Nanosecond)
        synctest.Wait()
        if err := ctx.Err(); err != nil {
            t.Fatalf("before timeout: %v", err)
        }

        time.Sleep(time.Nanosecond)
        synctest.Wait()
        if err := ctx.Err(); err != context.DeadlineExceeded {
            t.Fatalf("after timeout: got %v, want DeadlineExceeded", err)
        }
    })
}

在 Go 1.25+ 和 Go 1.26+ 中使用 synctest.Test。不要在 Go 1.25+ 或 Go 1.26+ 代码中使用旧的 Go 1.24 实验性 synctest.Run API。如果模块明确针对 Go 1.24 并启用了 GOEXPERIMENT=synctest,则仅作为兼容性回退使用旧 API。

synctest 的关键区别:

  • time.Sleep 在协程阻塞时立即推进合成时间
  • time.After 在合成时间到达持续时间时触发
  • 所有协程运行到阻塞点后时间才推进
  • 测试执行是确定性和可重复的

测试超时

对于可能挂起的测试,使用一个在调用者位置引发恐慌的超时辅助函数。请参见 辅助函数

基准测试

→ 有关高级基准测试:b.Loop()(Go 1.24+)、benchstat、从基准测试中分析以及 CI 回归检测,请参见 samber/cc-skills-golang@golang-benchmark 技能。

编写基准测试以衡量性能并检测回归:

func BenchmarkStringConcatenation(b *testing.B) {
    b.Run("plus-operator", func(b *testing.B) {
        for b.Loop() {
            result := "a" + "b" + "c"
            _ = result
        }
    })

    b.Run("strings.Builder", func(b *testing.B) {
        for b.Loop() {
            var builder strings.Builder
            builder.WriteString("a")
            builder.WriteString("b")
            builder.WriteString("c")
            _ = builder.String()
        }
    })
}

不同输入大小的基准测试:

func BenchmarkFibonacci(b *testing.B) {
    sizes := []int{10, 20, 30}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) {
            b.ReportAllocs()
            for b.Loop() {
                Fibonacci(size)
            }
        })
    }
}

对于 Go 1.24+,新基准测试应使用 b.Loop()。仅当模块目标为 Go <1.24 或有意保留旧基准测试代码时,才使用传统的 b.N 循环。

Go 1.26+:测试工件

当测试、基准测试或模糊测试目标需要持久化文件以供检查时,使用 ArtifactDir() 而非临时路径或仓库本地输出。

func TestRenderGoldenArtifact(t *testing.T) {
    dir := t.ArtifactDir()

    out := filepath.Join(dir, "rendered.json")
    if err := os.WriteFile(out, renderedBytes, 0o644); err != nil {
        t.Fatal(err)
    }

    t.Logf("artifact written: %s", out)
}

在 Go 1.26+ 中可用于 *testing.T*testing.B*testing.F

并行测试

使用 t.Parallel() 并发运行测试:

func TestParallelOperations(t *testing.T) {
    tests := []struct {
        name string
        data []byte
    }{
        {"small data", make([]byte, 1024)},
        {"medium data", make([]byte, 1024*1024)},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            is := assert.New(t)

            result := Process(tt.data)
            is.NotNil(result)
        })
    }
}

模糊测试

使用模糊测试发现边界情况和错误:

func FuzzReverse(f *testing.F) {
    f.Add("hello")
    f.Add("")
    f.Add("a")

    f.Fuzz(func(t *testing.T, input string) {
        reversed := Reverse(input)
        doubleReversed := Reverse(reversed)
        if input != doubleReversed {
            t.Errorf("Reverse(Reverse(%q)) = %q, want %q", input, doubleReversed, input)
        }
    })
}

示例作为文档

示例是由 go test 验证的可执行文档:

func ExampleCalculatePrice() {
    price := CalculatePrice(100, 10.0)
    fmt.Printf("Price: %.2f\n", price)
    // Output: Price: 900.00
}

func ExampleCalculatePrice_singleItem() {
    price := CalculatePrice(1, 25.50)
    fmt.Printf("Price: %.2f\n", price)
    // Output: Price: 25.50
}

代码覆盖率

# 生成覆盖率文件
go test -coverprofile=coverage.out ./...

# 以 HTML 形式查看覆盖率
go tool cover -html=coverage.out

# 按函数查看覆盖率
go tool cover -func=coverage.out

# 总覆盖率百分比
go tool cover -func=coverage.out | grep total

集成测试

使用构建标签将集成测试与单元测试分离:

//go:build integration

package mypackage

func TestDatabaseIntegration(t *testing.T) {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    // 测试真实数据库操作
}

单独运行集成测试:

go test -tags=integration ./...

有关 Docker Compose 测试夹具、SQL 模式和集成测试套件,请参见 集成测试

模拟

模拟接口,而非具体类型。在使用处定义接口,然后创建模拟实现。

有关模拟模式、测试夹具和时间模拟,请参见 模拟

使用 Linter 强制执行

许多测试最佳实践由 linter 自动强制执行:thelperparalleltesttestifylint。有关配置和使用,请参见 samber/cc-skills-golang@golang-lint 技能。

交叉引用

  • -> 有关详细的 testify API(assert、require、mock、suite),请参见 samber/cc-skills-golang@golang-stretchr-testify 技能
  • -> 有关数据库集成测试模式,请参见 samber/cc-skills-golang@golang-database 技能(testing.md
  • -> 有关使用 goleak 检测协程泄漏,请参见 samber/cc-skills-golang@golang-concurrency 技能
  • -> 有关 CI 测试配置和 GitHub Actions 工作流,请参见 samber/cc-skills-golang@golang-continuous-integration 技能
  • -> 有关 testifylint 和 paralleltest 配置,请参见 samber/cc-skills-golang@golang-lint 技能
  • -> 有关使用这些指南在 CI 中进行自动化 AI 驱动代码审查,请参见 samber/cc-skills-golang@golang-continuous-integration 技能

快速参考

go test ./...                          # 所有测试
go test -run TestName ./...            # 按精确名称运行特定测试
go test -run TestName/subtest ./...    # 测试内的子测试
go test -run 'Test(Add|Sub)' ./...     # 多个测试(正则 OR)
go test -run 'Test[A-Z]' ./...         # 以大写字母开头的测试
go test -run 'TestUser.*' ./...        # 匹配前缀的测试
go test -run '.*Validation.*' ./...    # 包含子字符串的测试
go test -run TestName/. ./...          # TestName 的所有子测试
go test -run '/(unit|integration)' ./... # 按子测试名称过滤
go test -race ./...                    # 竞态检测
go test -cover ./...                   # 覆盖率摘要
go test -bench=. -benchmem ./...       # 基准测试
go test -fuzz=FuzzName ./...           # 模糊测试
go test -tags=integration ./...        # 集成测试