关于 go-test-quality
go-test-quality 是面向 Go 开发者和编码 Agent 的测试技能,强调行为测试、可维护的测试夹具与并发安全,涵盖测试结构、模拟、竞态检查和基准测试。
主要功能
- 01以行为为中心的测试设计
- 02子测试、命名与文件布局
- 03辅助函数、夹具与清理指引
- 04模拟策略与测试类型选择
- 05并发、覆盖率与基准测试规范
适用场景
- 01为 Go 行为和边界情况设计测试
- 02测试 HTTP 处理器和数据库行为
- 03检查并发测试的波动与泄漏
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-test-quality安装后在 Claude Code 中输入/go-test-quality确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/concurrency-and-benchmarks.mdhelpers-and-fixtures.mdintegration-testing.md详细介绍
Go 测试质量
测试是生产代码。每次提交时,它们都会在 CI 中运行、记录行为,也是凌晨 3 点函数出问题时你首先会读的内容。 编写测试时,应像对待处理资金的代码一样认真。
社区默认规则。如果项目技能或 Agent 配置规则明确取代了
go-test-quality,则以该规则为准。
按需加载的详细参考资料:
references/helpers-and-fixtures.md— 测试辅助函数、带选项的工厂函数、t.Cleanup、黄金文件、模拟实现。references/integration-testing.md— httptest 记录器和服务、 testcontainers、构建标签、TestMain、模糊测试。references/concurrency-and-benchmarks.md—synctest、goleak、t.Context()、b.Loop()基准测试、t.Attr和工件。
只有在以下摘要不足以解答问题时,才阅读参考文件。
1. 测试设计理念
测试行为,而非实现
// ✅ Good — tests what the function DOES
func TestTransferFunds_InsufficientBalance(t *testing.T) {
from := NewAccount("alice", 100)
to := NewAccount("bob", 0)
err := TransferFunds(from, to, 150)
require.ErrorIs(t, err, ErrInsufficientFunds)
assert.Equal(t, 100, from.Balance(), "sender balance should be unchanged")
assert.Equal(t, 0, to.Balance(), "receiver balance should be unchanged")
}
// ❌ Bad — tests HOW the function does it
// asserts debit() was called before credit(), rollback() was called,
// internal mutex was locked — breaks on every refactor
每个逻辑概念只做一项断言
当多个 assert 调用用于验证同一行为的不同方面时(例如转账后双方账户的状态),完全没问题。一个测试同时检查创建、更新和删除,就是把三个测试伪装成一个。
像报告 bug 一样命名测试
测试失败时,只看名称就应知道出了什么问题:
// ✅ Good — reads like a sentence
func TestOrderService_Cancel_RefundsPartiallyShippedItems(t *testing.T) { ... }
func TestParseConfig_ReturnsErrorOnMissingRequiredField(t *testing.T) { ... }
// ❌ Bad — says nothing useful
func TestCancel(t *testing.T) { ... }
func TestRateLimiter_Success(t *testing.T) { ... }
布局
- 每个源文件配一个
_test.go文件(parser.go→parser_test.go),测试顺序与其覆盖的函数顺序一致——工具和代码审查者会按文件浏览。 - 公共 API 测试放在
pkg_test包中,这样它们只能使用调用方可用的内容;白盒测试留在pkg中。 - 集成测试使用
//go:build integration构建约束。
2. 用子测试组织场景
使用 t.Run 将相关场景归入一个父测试。每个子测试都有独立的初始化、失败信息,以及在 CI 输出中显示的名称:
func TestUserService_Create(t *testing.T) {
svc := setupUserService(t)
t.Run("succeeds with valid input", func(t *testing.T) {
user, err := svc.Create(ctx, CreateUserInput{Name: "Alice", Email: "[email protected]"})
require.NoError(t, err)
assert.NotEmpty(t, user.ID)
})
t.Run("rejects duplicate email", func(t *testing.T) {
_, _ = svc.Create(ctx, CreateUserInput{Name: "Alice", Email: "[email protected]"})
_, err := svc.Create(ctx, CreateUserInput{Name: "Bob", Email: "[email protected]"})
require.ErrorIs(t, err, ErrDuplicateEmail)
})
}
3. 测试辅助函数规则
- 始终在测试工具函数中调用
t.Helper(),这样失败位置会指向调用方,而不是辅助函数。 - 为复杂测试对象使用带函数选项的工厂函数——提供默认值并允许各测试单独覆盖,绝不要使用有 15 个参数的构造函数。
- 优先使用
t.Cleanup而不是defer——即使在调用t.FailNow()后,它也会运行,并且作用范围是测试,而非函数。
完整实现见 references/helpers-and-fixtures.md。
4. 选择测试类型
| 场景 | 方法 | 详情 |
|---|---|---|
| 纯函数,有 3 个或更多数据用例 | 表驱动测试 | go-test-table-driven 技能 |
| 隔离测试 HTTP 处理器 | httptest.NewRecorder + 模拟存储 | references/integration-testing.md |
| 完整路由/中间件栈 | httptest.NewServer | references/integration-testing.md |
| 真实数据库行为 | testcontainers + 构建标签 | references/integration-testing.md |
| 复杂输出(JSON、HTML、SQL) | testdata/ 中的黄金文件 | references/helpers-and-fixtures.md |
| 针对不可信输入的解析器/验证器 | 模糊测试 | references/integration-testing.md |
5. 模拟规则
- 对小型接口(≤3 个方法)使用基于接口的手写模拟:结构体包含函数字段,并记录调用情况。
- 简单的接缝使用函数注入(
now func() time.Time)。 - 不要模拟: 值对象、纯函数、标准库,或同一包中的自有代码。测试真实对象。
- 如果什么都模拟,你测到的就是模拟对象,而不是自己的代码。
6. 并行执行与覆盖率
func TestSlugify(t *testing.T) {
t.Parallel() // safe: pure function, no shared state
// ...
}
当测试共享可变状态、数据库、文件或进程级状态时,不要使用 t.Parallel()——这正是 t.Setenv 和 t.Chdir 在并行测试中会 panic 的原因。
go test -race -coverprofile=coverage.out -coverpkg=./... ./...
go tool cover -html=coverage.out # read the red lines, not the percentage
覆盖率可以找出未测试的代码,但无法衡量断言质量。用它来定位缺口,并让 CI 强制执行一个只增不减的最低门槛(门槛由 go-ci 技能负责)。不使用 -coverpkg 时,仅由其他包测试覆盖的包会报告 0%。
7. 测试并发代码
- 切勿用
time.Sleep同步测试——即使有效也会拖慢测试,无效时还会导致测试不稳定。 - Go 1.25+:在
synctest.Test中运行依赖时间的代码。当所有 goroutine 都被阻塞后,沙箱的虚拟时钟会立即前进;synctest.Wait()可替代“休眠,直到后台 goroutine 到达目标状态”。在 Go 1.24 中,该 API 是通过GOEXPERIMENT=synctest启用的synctest.Run。Go 1.27 新增了synctest.Sleep(d)(一次调用即可推进时间并等待状态稳定)和httptest.NewTestServer(t, h),这是一个可在沙箱中运行并会自行关闭的内存服务器。 - 启动 goroutine 的包都必须验证没有 goroutine 泄漏:在
TestMain中调用goleak.VerifyTestMain(m),或使用defer goleak.VerifyNone(t)。-race可以发现数据竞争,但无法发现永远不会退出的 goroutine。
8. 测试上下文、基准测试与构件
- 使用
t.Context()(Go 1.24+)代替context.Background()——它会在t.Cleanup运行前取消,因此测试结束时,阻塞中的工作会退出。 - 基准测试使用
for b.Loop()(Go 1.24+):初始化只运行一次,结果无需借助接收变量也能保持存活。添加b.ReportAllocs(),并用benchstat比较运行结果。 t.Attr(Go 1.25+)会输出 CI 可解析的键值对;使用go test -artifacts(Go 1.26+)时,t.ArtifactDir()会将失败的黄金输出或捕获的响应体保存在仓库之外。
这两节的示例见:references/concurrency-and-benchmarks.md。
9. 断言库陷阱
- 在每个
t.Run内创建assert.New(t)/require.New(t)。 从父级t创建的实例会将子测试的失败报告到父级,而失败的子测试本身却会显示PASS。 require、t.Fatal和t.FailNow只能在测试 goroutine 中使用。 在启动的 goroutine 中调用它们无法停止测试。应通过通道传回结果,并在接收后进行断言。go vet的testinggoroutine和 golangci-lint 的testifylint可捕获大多数此类问题。- 前置条件使用
require,独立检查使用assert——前置条件失败时应停止测试,以免触发 nil 指针异常。 assert.Eventually会掩盖轮询中的时序问题;优先使用synctest。
反模式
- 🔴 没有断言的测试——总是通过,毫无证明作用
- 🔴 用
time.Sleep同步——改用testing/synctest、通道或轮询 - 🔴 测试依赖执行顺序——每个测试都必须能独立运行
- 🔴 什么都模拟——最终测试的是模拟对象,而不是代码
- 🟡 使用
Test1、TestSuccess之类的测试名称——应为场景命名 - 🟡 访问私有字段——通过公共 API 测试
- 🟡 没有边界用例:空值、nil、零值、最大值、Unicode
- 🟡 共用的初始化代码过于庞大——每个测试只应初始化所需内容
- 🟡 在测试中使用
context.Background()——改用t.Context() - 🟡 在 Go 1.24+ 中使用
for range b.N——改用for b.Loop() - 🟢 对所有接收不可信输入的代码进行模糊测试
- 🟢 使用黄金文件比较复杂输出
验证清单
- 每个测试都有有意义的断言(没有空测试主体)
- 测试名称描述场景,而不是方法
- 每个测试工具函数都调用
t.Helper() - 使用
t.Cleanup()清理资源 - 在安全时使用
t.Parallel(),不安全时避免使用 - 集成测试通过
testing.Short()或构建标签进行控制 - 模拟保持精简——仅模拟外部依赖
- 整个测试套件中没有使用
time.Sleep同步 - 启动 goroutine 的包使用
goleak进行验证 - 基准测试使用
for b.Loop()并调用b.ReportAllocs() - 覆盖边界用例:空值、nil、零值、边界值
go test -race ./...通过- 使用覆盖率查找遗漏;CI 应强制执行逐步提高的最低覆盖率,而不是固定目标
- testify 实例按子测试创建;不在测试 goroutine 之外调用
require