关于 golang-testing
golang-testing 是一项用于设计、编写、审查、审计和调试 Go 测试的技能。它强调验证行为、隔离性、确定性和测试套件可维护性,并为不同任务提供相应流程。
主要功能
- 01编写、审查、审计和调试模式
- 02命名表驱动测试的指导
- 03通过构建标签隔离集成测试
- 04并发与 Goroutine 泄漏检查
- 05模糊测试、基准测试与覆盖率指导
适用场景
- 01为新的 Go 行为编写测试
- 02审查拉取请求中的测试变更
- 03审计测试套件中的缺口或不稳定项
- 04诊断失败或不稳定的测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-testing安装后在 Claude Code 中输入/golang-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/benchmarks.mdcoverage.mdexamples.mdhelpers.mdhttp-testing.mdintegration-testing.mdmocking.md详细介绍
角色: 你是一名 Go 工程师,将测试视为可执行规范。编写测试是为了约束行为,而不是追求覆盖率指标。
思考模式: 为测试策略设计和故障分析进行尽可能全面的推理——浅层推理会遗漏边缘情况,导致测试脆弱:今天能通过,明天就可能失效。在 Claude Code 中,使用 ultrathink 显式触发扩展思考。
编排模式: 审计大型测试套件时,启动审计模式中描述的三个子 Agent(单元测试质量与覆盖缺口、集成测试隔离、goroutine/竞态问题),并将其发现汇总为一份缺口报告。在 Claude Code 中,使用 ultracode 显式启用多 Agent 编排。
模式:
- 编写模式——为现有或新代码编写测试。按顺序检查被测代码;使用
gotests搭建表驱动测试,然后补充边缘情况和错误路径。 - 评审模式——评审 PR 中的测试变更。聚焦差异:检查新行为的覆盖情况、断言质量、表驱动结构,以及是否存在不稳定模式。按顺序进行。
- 审计模式——审计现有测试套件中的缺口、不稳定因素或不良模式(依赖顺序的测试、缺少
t.Parallel()、与实现细节耦合)。最多启动 3 个并行子 Agent,按关注点分工:(1) 单元测试质量与覆盖缺口,(2) 集成测试隔离与构建标签,(3) goroutine 泄漏与竞态条件。 - 调试模式——测试失败或不稳定时使用。按顺序进行:可靠地复现问题,定位失败的断言,并在生产代码或测试设置中追踪根因。
社区默认项。 如果某项公司技能明确取代了
samber/cc-skills-golang@golang-testing技能,则优先采用该技能。
依赖项:
- gotests:
go install github.com/cweill/gotests/gotests@latest
Go 测试最佳实践
本技能指导如何为 Go 应用创建可用于生产环境的测试。遵循这些原则,编写易于维护、快速且可靠的测试。
最佳实践摘要
- 表驱动测试 MUST 使用带名称的子测试——每个测试用例都需要一个
name字段,并传入t.Run - 集成测试 MUST 使用构建标签(
//go:build integration),与单元测试分开 - 测试 MUST NOT 依赖执行顺序——每个测试都 MUST 能够独立运行
- 在可能的情况下,彼此独立的测试 SHOULD 使用
t.Parallel() - 测试 MUST 断言可观察行为和公共 API 契约,而非实现细节——与内部实现耦合的测试会让每次重构都变成重写测试,却无法证明任何契约
- 含有 goroutine 的包 SHOULD 在
TestMain中使用goleak.VerifyTestMain检测 goroutine 泄漏 - 将 testify 用作辅助工具,而非标准库的替代品
- 模拟接口,而非具体类型
- 保持单元测试快速(< 1 ms),使用构建标签标记集成测试
- 在 CI 中启用竞态检测运行测试
- 将示例作为可执行文档
- 测试文件 MUST 以被测源文件命名,而非以被测函数或方法命名
- 测试函数 SHOULD 按其在源文件中对应的函数/方法顺序排列
测试结构与组织
文件约定
// package_test.go - tests in same package (white-box, access unexported)
package mypackage
// mypackage_test.go - tests in test package (black-box, public API only)
package mypackage_test
测试文件应以其所测试的源文件命名,而不是以被测函数或方法命名。Go 的惯例是每个源文件对应一个测试文件(foo.go -> foo_test.go),因为工具(go test、覆盖率报告、IDE 中“跳转到测试”的导航、gotests)和代码审查者都是按源文件而非符号来定位测试。一个源文件通常会声明多个函数或方法;按符号名称拆分测试会让测试分散到多个文件中,破坏源文件与测试文件之间的映射关系。
// ✓ Good — one test file per source file
helloworld.go -> helloworld_test.go // contains TestHelloWorld, TestAbcd, TestXyz, ...
// ✗ Bad — test file named after the function/method instead of the source file
helloworld.go -> abcd_test.go // wrong: should be helloworld_test.go
例外:非常大的源文件可以按关注点拆分为多个 _test.go 文件(例如 foo_test.go + foo_edgecases_test.go),但每个拆分文件的名称仍必须由源文件名派生,绝不能使用单个函数名。即使文件变得很大,也应尽量为每个源文件保留一个 _test.go 文件——拆分会增加导航开销,通常得不偿失;只有当单个文件确实难以浏览或审查时,才考虑此例外。
在测试文件中,测试函数的顺序应与它们所测试的函数或方法在源文件中的顺序一致。这样,读者(无论是人还是 Agent)在同时滚动查看 foo.go 和 foo_test.go 时,就能按位置找到对应测试,而不必搜索;两者的顺序一旦偏离,每当任一文件增长,这种偏差都会进一步加剧。
命名约定
func TestAdd(t *testing.T) { ... } // function test
func TestMyStruct_MyMethod(t *testing.T) { ... } // method test
func BenchmarkAdd(b *testing.B) { ... } // benchmark
func ExampleAdd() { ... } // example
func FuzzAdd(f *testing.F) { ... } // fuzz test
表驱动测试
表驱动测试是 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)
}
})
}
}
常见陷阱:断言作用域泄漏到子测试
绝不要在父测试函数中创建 testify assert/require 实例,然后在 t.Run 闭包中重复使用。assert.New(t) 会捕获创建它时所用的确切 *testing.T;如果这个 t 属于父测试,那么子测试中引发的所有失败都会在 go test 输出中归到_父_测试名下——失败的子测试本身仍会显示 --- PASS,从而悄悄掩盖究竟哪个用例出错。无论子测试是否调用 t.Parallel(),都会发生这种情况。
// WRONG -- `is` is bound to the parent's t
func TestCalculatePrice(t *testing.T) {
is := assert.New(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice)) // misattributed on failure
})
}
}
// RIGHT -- each subtest builds its own instance from its own t
func TestCalculatePrice(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
is := assert.New(t)
is.Equal(tt.expected, CalculatePrice(tt.quantity, tt.unitPrice))
})
}
}
可以通过一个故意失败的用例进行验证:如果 go test -v -run TestName 显示 --- FAIL: TestName,但每一行 --- PASS: TestName/subtest_name 仍显示 PASS,那么断言作用域就发生了泄漏。
单元测试
单元测试应当快速(< 1 ms)、隔离(无外部依赖)且结果确定。
测试 HTTP 处理程序
使用 httptest,并采用表驱动模式测试处理程序。有关请求/响应正文、查询参数、请求头和状态码断言的示例,请参阅 HTTP 测试。
使用 goleak 检测 Goroutine 泄漏
使用 go.uber.org/goleak 检测泄漏的 Goroutine,尤其是并发代码中的泄漏:
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
若要排除特定的 Goroutine 堆栈(针对已知泄漏或库中的 Goroutine):
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
goleak.IgnoreCurrent(),
)
}
也可以按测试排除:
func TestWorkerPool(t *testing.T) {
defer goleak.VerifyNone(t)
// ... test code ...
}
用于确定性 Goroutine 测试的 testing/synctest
testing/synctest(Go 1.25+)可为 Goroutine、定时器、截止时间和上下文取消提供确定性测试。只有当所有 Goroutine 都处于阻塞状态时,时间才会推进,因此执行顺序可预测。
以下情况适合使用 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 及更高版本中使用 synctest.Test。不要在 Go 1.25+ 代码中使用旧的 Go 1.24 实验性 synctest.Run API。如果模块明确以 Go 1.24 为目标,并启用了 GOEXPERIMENT=synctest,则仅将旧 API 用作兼容性回退方案。
synctest 的主要区别:
- Goroutine 阻塞时,
time.Sleep会立即推进模拟时间 - 模拟时间达到指定时长时,
time.After会触发 - 时间推进前,所有 Goroutine 都会运行到阻塞点
- 测试执行具有确定性且可重复
- Go 1.27+ 新增
synctest.Sleep(d),可直接推进 bubble 的模拟时钟;它等同于先调用time.Sleep(d)再调用synctest.Wait(),但不需要真实 Goroutine 阻塞
Go 1.27+ 还新增了 httptest.NewTestServer(),这是 httptest.NewServer 的内存中虚拟网络版本,可与 synctest 配合使用——它不使用真实套接字,因此服务器测试可以在 synctest.Test bubble 中运行,而不必使用 httptest.NewServer 和真实定时器。
测试超时
对于可能挂起的测试,请使用会附带调用位置并触发 panic 的超时辅助函数。参见 Helpers。
基准测试
将基准测试写成子基准测试(每个变体使用一个 b.Run),这样每个变体在输出中都有自己的名称——比较工具会根据该名称进行差异比较。对于 Go 1.24+,使用 b.Loop(),而不是 b.N 循环。
→ 有关代码结构和按大小参数化的示例,请参见 Benchmarks in a Test Suite。
→ 有关测量方法,请参见 samber/cc-skills-golang@golang-benchmark skill:benchstat、基于基准测试的性能分析,以及 CI 回归检测。
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 均提供此功能。
Go 1.27+:自动运行 stdversion
现在,go test 默认会调用 stdversion vet 检查,标记任何使用了比模块 go 指令所指定版本更新的 API 的情况。此检查导致 CI 失败,意味着需要提升 go 指令指定的版本,或停止使用较新的 API——不应试图屏蔽此检查。
并行测试
使用 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)
})
}
}
模糊测试
使用模糊测试查找边界情况和 Bug:
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)
}
})
}
将示例作为文档
ExampleXxx 函数是可执行文档:go test 会将其标准输出与 // Output: 注释进行比较,因此示例发生偏差时,构建会失败,而不是误导读者。
→ 有关命名规则、Unordered output 和放置位置,请参见 Examples as Documentation。
代码覆盖率
使用 go test -coverprofile=coverage.out ./... 生成覆盖率配置文件,然后通过 go tool cover -html=coverage.out 查看未覆盖的代码行。覆盖率可以定位未测试的路径,但无法衡量断言质量,因此应将覆盖率百分比视为查找缺口的工具,而非目标。
→ 有关覆盖率模式、-coverpkg 和报告中的常见问题,请参见 Code Coverage。
集成测试
使用构建标签将集成测试与单元测试分开:
//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()
// Test real database operations
}
单独运行集成测试:
go test -tags=integration ./...
有关 Docker Compose 测试夹具、SQL 模式和集成测试套件,请参见 Integration Testing。
模拟
模拟接口,而非具体类型。在使用接口的地方定义接口,然后创建模拟实现。
有关模拟模式、测试夹具和时间模拟,请参阅 模拟。
使用 Linter 强制执行
许多测试最佳实践可通过 Linter 自动强制执行:thelper、paralleltest、testifylint。有关配置和用法,请参阅 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 检测 goroutine 泄漏,请参阅
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 ./... # all tests
go test -run TestName ./... # specific test by exact name
go test -run TestName/subtest ./... # subtests within a test
go test -run 'Test(Add|Sub)' ./... # multiple tests (regexp OR)
go test -run 'Test[A-Z]' ./... # tests starting with capital letter
go test -run 'TestUser.*' ./... # tests matching prefix
go test -run '.*Validation.*' ./... # tests containing substring
go test -run TestName/. ./... # all subtests of TestName
go test -run '/(unit|integration)' ./... # filter by subtest name
go test -race ./... # race detection
go test -cover ./... # coverage summary
go test -bench=. -benchmem ./... # benchmarks
go test -fuzz=FuzzName ./... # fuzzing
go test -tags=integration ./... # integration tests