mcpField
G
go-test-quality

go-test-quality

github.com/eduardo-sl测试与质量83198 次安装2 周前更新
代码仓库

关于 go-test-quality

go-test-quality 是面向 Go 开发者和编码 Agent 的测试技能,强调行为测试、可维护的测试夹具与并发安全,涵盖测试结构、模拟、竞态检查和基准测试。

主要功能

  1. 01以行为为中心的测试设计
  2. 02子测试、命名与文件布局
  3. 03辅助函数、夹具与清理指引
  4. 04模拟策略与测试类型选择
  5. 05并发、覆盖率与基准测试规范

适用场景

  1. 01为 Go 行为和边界情况设计测试
  2. 02测试 HTTP 处理器和数据库行为
  3. 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.md
helpers-and-fixtures.md
integration-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. 测试辅助函数规则

  1. 始终在测试工具函数中调用 t.Helper(),这样失败位置会指向调用方,而不是辅助函数。
  2. 为复杂测试对象使用带函数选项的工厂函数——提供默认值并允许各测试单独覆盖,绝不要使用有 15 个参数的构造函数。
  3. 优先使用 t.Cleanup 而不是 defer——即使在调用 t.FailNow() 后,它也会运行,并且作用范围是测试,而非函数。

完整实现见 references/helpers-and-fixtures.md。

4. 选择测试类型

场景方法详情
纯函数,有 3 个或更多数据用例表驱动测试go-test-table-driven 技能
隔离测试 HTTP 处理器httptest.NewRecorder + 模拟存储references/integration-testing.md
完整路由/中间件栈httptest.NewServerreferences/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()
  • 🟢 对所有接收不可信输入的代码进行模糊测试
  • 🟢 使用黄金文件比较复杂输出

验证清单

  1. 每个测试都有有意义的断言(没有空测试主体)
  2. 测试名称描述场景,而不是方法
  3. 每个测试工具函数都调用 t.Helper()
  4. 使用 t.Cleanup() 清理资源
  5. 在安全时使用 t.Parallel(),不安全时避免使用
  6. 集成测试通过 testing.Short() 或构建标签进行控制
  7. 模拟保持精简——仅模拟外部依赖
  8. 整个测试套件中没有使用 time.Sleep 同步
  9. 启动 goroutine 的包使用 goleak 进行验证
  10. 基准测试使用 for b.Loop() 并调用 b.ReportAllocs()
  11. 覆盖边界用例:空值、nil、零值、边界值
  12. go test -race ./... 通过
  13. 使用覆盖率查找遗漏;CI 应强制执行逐步提高的最低覆盖率,而不是固定目标
  14. testify 实例按子测试创建;不在测试 goroutine 之外调用 require

评论

支持 Markdown