关于 go-testing-code-review
go-testing-code-review 是用于审查 Go 测试及相关代码变更的技能。它要求审查者先确认 Go 版本、阅读测试上下文并按适用清单检查,再核实问题后报告。
主要功能
- 01按顺序执行的审查流程
- 02结合 Go 版本的测试建议
- 03测试结构与覆盖率清单
- 04Mock 与测试隔离指引
- 05带严重级别的问题格式
适用场景
- 01审查 Go 测试文件的变更
- 02检查并行测试是否共享可变状态
- 03评估 HTTP、模糊、基准或黄金文件测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill go-testing-code-review安装后在 Claude Code 中输入/go-testing-code-review确认已加载。
附带文件references/mocking.mdreferences/structure.md
详细介绍
Go 测试代码审查
审查工作流
按以下顺序执行。满足下面所有 通过 条件之前,不要输出任何问题。
-
基准
go.mod— 打开待审查模块的go.mod,并读取go指令。
通过: 可以准确写出go X.YY的值(在审查前言或工作笔记中)。仅当版本与此基准相符时,才应用受版本限制的建议(例如,Go 1.18+ 支持模糊测试,Go 1.22 之前的循环变量捕获)。 -
阅读相关测试 — 对于范围内的每个
*_test.go(或基准测试/模糊测试文件),阅读完整的测试函数及其使用的任何表格struct{...}/辅助函数,不要只看差异代码块。
通过: 范围内的每个文件中,至少有一个包含变更的完整func Test.../func Benchmark.../func Fuzz...(或它调用的辅助函数)已被阅读。 -
确定清单范围 — 判断审查清单中的哪些条目适用(表驱动结构、并行测试、HTTP、黄金文件、模拟对象)。针对这些主题,打开 references/structure.md 和/或 references/mocking.md;对于与差异无关的条目,说明不适用的原因(一句话即可,例如“变更中没有
t.Parallel”)。
通过: 审查(或工作笔记)列出了应用的清单主题,或说明了不适用主题及其与差异相关的原因。 -
报告前验证 — 加载并遵循 review-verification-protocol。
通过: 对于要报告的每个问题,均满足该协议的报告前验证清单(已阅读实际测试代码、检查了上下文、区分了“错误”与“风格不同”等)。
强制关卡(相同顺序,简明版)
| 步骤 | 客观通过条件 |
|---|---|
| 1 | 在给出特定版本的测试建议前,已记录 go.mod 中的 go X.YY。 |
| 2 | 已阅读范围内每个测试文件中的完整外围测试(或它使用的辅助函数),而非只看差异。 |
| 3 | 已列出适用的清单主题,或说明不适用主题及其与差异相关的原因;按需打开参考文档。 |
| 4 | 对每个报告的问题,均已完成 review-verification-protocol。 |
输出格式
按以下格式报告问题:
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
快速参考
| 问题类型 | 参考文档 |
|---|---|
| 测试结构、命名 | references/structure.md |
| 模拟对象、接口 | references/mocking.md |
审查清单
- 测试采用表驱动形式,且用例名称清晰
- 子测试使用 t.Run 以便并行执行
- 测试名称描述行为,而非实现方式
- 错误信息包含 got/want,并有描述性说明
- 使用 t.Cleanup 注册清理操作
- 并行测试不共享可变状态
- 模拟对象使用测试文件中定义的接口
- 覆盖边界情况和错误路径
- 性能关键函数有
Benchmark*测试 - 输入解析器/验证器有
Fuzz*测试(Go 1.18+) - HTTP 处理器使用
httptest.NewRequest/httptest.NewRecorder测试 - 黄金文件测试采用
testdata/*.golden模式,并提供-update标志
关键模式
表驱动测试
// BAD - repetitive
func TestAdd(t *testing.T) {
if Add(1, 2) != 3 {
t.Error("wrong")
}
if Add(0, 0) != 0 {
t.Error("wrong")
}
}
// GOOD
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 1, 2, 3},
{"zeros", 0, 0, 0},
{"negative", -1, 1, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
错误信息
// BAD
if got != want {
t.Error("wrong result")
}
// GOOD
if got != want {
t.Errorf("GetUser(%d) = %v, want %v", id, got, want)
}
// For complex types
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetUser() mismatch (-want +got):\n%s", diff)
}
并行测试
func TestFoo(t *testing.T) {
tests := []struct{...}
for _, tt := range tests {
tt := tt // capture (not needed Go 1.22+)
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// test code
})
}
}
清理操作
// BAD - manual cleanup, skipped on failure
func TestWithTempFile(t *testing.T) {
f, _ := os.CreateTemp("", "test")
defer os.Remove(f.Name()) // skipped if test panics
}
// GOOD
func TestWithTempFile(t *testing.T) {
f, _ := os.CreateTemp("", "test")
t.Cleanup(func() {
os.Remove(f.Name())
})
}
其他模式
基准测试
func BenchmarkProcess(b *testing.B) {
data := generateTestData(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Process(data)
}
}
// Run: go test -bench=BenchmarkProcess -benchmem
模糊测试(Go 1.18+)
func FuzzParseInput(f *testing.F) {
// Seed corpus
f.Add(`{"name": "test"}`)
f.Add(``)
f.Add(`{invalid}`)
f.Fuzz(func(t *testing.T, input string) {
result, err := ParseInput(input)
if err != nil {
return // invalid input is expected
}
// If parsing succeeded, re-encoding should work
if _, err := json.Marshal(result); err != nil {
t.Errorf("Marshal after Parse: %v", err)
}
})
}
// Run: go test -fuzz=FuzzParseInput -fuzztime=30s
HTTP 处理器测试
func TestHandler(t *testing.T) {
srv := NewServer(mockDeps)
req := httptest.NewRequest("GET", "/api/users/123", nil)
w := httptest.NewRecorder()
srv.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
黄金文件
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
got := Render(input)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
if err := os.WriteFile(golden, got, 0644); err != nil {
t.Fatalf("writing golden file: %v", err)
}
}
want, err := os.ReadFile(golden)
if err != nil {
t.Fatalf("reading golden file: %v (run with -update to create)", err)
}
if !bytes.Equal(got, want) {
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
}
}
反模式
1. 测试内部实现
// BAD - tests private state
func TestUser(t *testing.T) {
u := NewUser("alice")
if u.id != 1 { // testing internal field
t.Error("wrong id")
}
}
// GOOD - tests behavior
func TestUser(t *testing.T) {
u := NewUser("alice")
if u.ID() != 1 {
t.Error("wrong ID")
}
}
2. 共享可变状态
// BAD - tests interfere with each other
var testDB = setupDB()
func TestA(t *testing.T) {
t.Parallel()
testDB.Insert(...) // race!
}
// GOOD - isolated per test
func TestA(t *testing.T) {
db := setupTestDB(t)
t.Cleanup(func() { db.Close() })
db.Insert(...)
}
3. 缺少上下文的断言
// BAD
assert.Equal(t, want, got) // "expected X got Y" - which test?
// GOOD
assert.Equal(t, want, got, "user name after update")
何时加载参考文档
- 审查测试文件结构 → structure.md
- 审查模拟对象实现 → mocking.md
审查问题
- 测试是否采用表驱动形式,并为用例命名?
- 错误信息是否包含输入、got 和 want?
- 并行测试是否彼此隔离(没有共享状态)?
- 是否通过 t.Cleanup 执行清理?
- 测试验证的是行为,而非实现方式?