npx skills add https://github.com/cxuu/golang-skills --skill go-testing遵循 Google 风格编写清晰、可维护的 Go 测试的指南。
规范性要求:测试失败信息必须无需阅读测试源码即可诊断。
每个失败信息都应包含:
使用标准格式:YourFunc(%v) = %v, want %v
// Good:
if got := Add(2, 3); got != 5 {
t.Errorf("Add(2, 3) = %d, want %d", got, 5)
}
// Bad: 缺少函数名和输入参数
if got := Add(2, 3); got != 5 {
t.Errorf("got %d, want %d", got, 5)
}
始终先打印实际结果,再打印预期结果:
// Good:
t.Errorf("Parse(%q) = %v, want %v", input, got, want)
// Bad: want/got 顺序颠倒了
t.Errorf("Parse(%q) want %v, got %v", input, want, got)
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
规范性要求:不要创建或使用断言库。
断言库会分散开发者的经验,并且通常会产生无用的失败信息。
// Bad:
assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
// Good: 使用 cmp 包和标准比较
want := BlogPost{
Type: "blogPost",
Comments: 2,
Body: "Hello, world!",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetPost() mismatch (-want +got):\n%s", diff)
}
对于特定领域的比较,应返回值或错误,而不是调用 t.Error:
// Good: 返回值用于失败信息
func postLength(p BlogPost) int { return len(p.Body) }
func TestBlogPost(t *testing.T) {
post := BlogPost{Body: "Hello"}
if got, want := postLength(post), 5; got != want {
t.Errorf("postLength(post) = %v, want %v", got, want)
}
}
建议:对于复杂类型,优先使用
cmp.Equal和cmp.Diff。
// Good: 完整的结构体比较并输出差异 - 始终包含方向标识
want := &Doc{Type: "blogPost", Authors: []string{"isaac", "albert"}}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("AddPost() mismatch (-want +got):\n%s", diff)
}
// Good: Protocol buffers
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("Foo() mismatch (-want +got):\n%s", diff)
}
避免不稳定的比较 - 不要比较可能发生变化的 JSON/序列化输出。应进行语义上的比较。
规范性要求:使用
t.Error让测试继续运行;仅当无法继续时才使用t.Fatal。
测试应在单次运行中报告所有失败:
// Good: 报告所有不匹配项
if diff := cmp.Diff(wantMean, gotMean); diff != "" {
t.Errorf("Mean mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(wantVariance, gotVariance); diff != "" {
t.Errorf("Variance mismatch (-want +got):\n%s", diff)
}
当后续测试变得毫无意义时,使用 t.Fatal:
// Good: 在设置失败或继续执行无意义时使用 Fatal
gotEncoded := Encode(input)
if gotEncoded != wantEncoded {
t.Fatalf("Encode(%q) = %q, want %q", input, gotEncoded, wantEncoded)
// 解码意外的输出是没有意义的
}
gotDecoded, err := Decode(gotEncoded)
if err != nil {
t.Fatalf("Decode(%q) error: %v", gotEncoded, err)
}
规范性要求:切勿在测试 goroutine 以外的 goroutine 中调用
t.Fatal、t.Fatalf或t.FailNow。应使用t.Error并让测试继续运行。
建议:当许多测试用例共享相似逻辑时,使用表驱动测试。
// Good:
func TestCompare(t *testing.T) {
tests := []struct {
a, b string
want int
}{
{"", "", 0},
{"a", "", 1},
{"", "a", -1},
{"abc", "abc", 0},
}
for _, tt := range tests {
got := Compare(tt.a, tt.b)
if got != tt.want {
t.Errorf("Compare(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
}
}
}
当测试用例跨越多行或具有相同类型的相邻字段时,使用字段名。
不要通过索引标识行 - 在失败信息中包含输入参数,而不是使用 Case #%d failed。
来源:Uber Go 风格指南
当测试用例需要复杂的设置、条件模拟或多个分支时,应优先选择单独的测试函数,而不是表驱动测试。
// Bad: 过多的条件字段使测试难以理解
tests := []struct {
give string
want string
wantErr error
shouldCallX bool // 条件逻辑标志
shouldCallY bool // 另一个条件标志
giveXResponse string
giveXErr error
giveYResponse string
giveYErr error
}{...}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
if tt.shouldCallX { // 条件模拟设置
xMock.EXPECT().Call().Return(tt.giveXResponse, tt.giveXErr)
}
if tt.shouldCallY { // 更多分支
yMock.EXPECT().Call().Return(tt.giveYResponse, tt.giveYErr)
}
// ...
})
}
// Good: 独立的、聚焦的测试更清晰
func TestShouldCallX(t *testing.T) {
xMock.EXPECT().Call().Return("XResponse", nil)
got, err := DoComplexThing("inputX", xMock, yMock)
// assert...
}
func TestShouldCallYAndFail(t *testing.T) {
yMock.EXPECT().Call().Return("YResponse", nil)
_, err := DoComplexThing("inputY", xMock, yMock)
// assert error...
}
表驱动测试在以下情况效果最佳:
如果测试主体简短且直接,使用单个 shouldErr 字段来表示成功/失败是可以接受的。
建议:使用子测试以获得更好的组织、过滤和并行执行。
t.Run("empty_input", ...), t.Run("hu_to_en", ...)// Good: 带有子测试的表驱动测试
func TestTranslate(t *testing.T) {
tests := []struct {
name, srcLang, dstLang, input, want string
}{
{"hu_en_basic", "hu", "en", "köszönöm", "thank you"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Translate(tt.srcLang, tt.dstLang, tt.input); got != tt.want {
t.Errorf("Translate(%q, %q, %q) = %q, want %q",
tt.srcLang, tt.dstLang, tt.input, got, tt.want)
}
})
}
}
来源:Uber Go 风格指南
在表驱动测试中使用 t.Parallel() 时,请注意循环变量捕获问题:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Go 1.22+: tt 在每次迭代中都能正确捕获
// Go 1.21-: 在此处添加 "tt := tt" 以捕获变量
got := Process(tt.give)
if got != tt.want {
t.Errorf("Process(%q) = %q, want %q", tt.give, got, tt.want)
}
})
}
规范性要求:测试辅助函数必须调用
t.Helper(),并且对于设置失败应使用t.Fatal。
// Good: 完整的测试辅助函数模式
func mustLoadTestData(t *testing.T, filename string) []byte {
t.Helper() // 使失败信息指向调用者
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("Setup failed: could not read %s: %v", filename, err)
}
return data
}
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Could not open database: %v", err)
}
t.Cleanup(func() { db.Close() }) // 使用 t.Cleanup 进行清理
return db
}
关键规则:
t.Helper() 以将失败归因于调用者t.Fatal(不要返回错误)t.Cleanup() 进行清理,而不是 defer建议:为测试替身(stubs、fakes、mocks、spies)遵循一致的命名规则。
包命名:在生产包名后附加 test(例如 creditcardtest)。
// Good: 在 creditcardtest 包中
// 单个替身 - 使用简单名称
type Stub struct{}
func (Stub) Charge(*creditcard.Card, money.Money) error { return nil }
// 多种行为 - 按行为命名
type AlwaysCharges struct{}
type AlwaysDeclines struct{}
// 多种类型 - 包含类型名
type StubService struct{}
type StubStoredValue struct{}
局部变量:为清晰起见,为测试替身变量添加前缀(使用 spyCC 而不是 cc)。
| 包声明 | 使用场景 |
|---|---|
package foo | 同包测试,可以访问未导出的标识符 |
package foo_test | 黑盒测试,避免循环依赖 |
两者都放在 foo_test.go 文件中。当仅测试公共 API 或需要打破导入循环时,使用 _test 后缀。
建议:测试错误的语义,而不是错误信息字符串。
// Bad: 脆弱的字符串比较
if err.Error() != "invalid input" {
t.Errorf("unexpected error: %v", err)
}
// Good: 测试语义错误
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("got error %v, want ErrInvalidInput", err)
}
// Good: 当语义不重要时进行简单的存在性检查
if gotErr := err != nil; gotErr != tt.wantErr {
t.Errorf("f(%v) error = %v, want error presence = %t", tt.input, err, tt.wantErr)
}
建议:将设置的作用域限定在需要它的测试中。
// Good: 在需要它的测试中显式设置
func TestParseData(t *testing.T) {
data := mustLoadDataset(t)
// ...
}
func TestUnrelated(t *testing.T) {
// 不承担数据集加载的开销
}
// Bad: 全局 init 为所有测试加载数据
var dataset []byte
func init() {
dataset = mustLoadDataset() // 即使是不相关的测试也会运行
}
| 场景 | 方法 |
|---|---|
| 比较结构体/切片 | cmp.Diff(want, got) |
| 简单的值不匹配 | t.Errorf("F(%v) = %v, want %v", in, got, want) |
| 设置失败 | t.Fatalf("Setup: %v", err) |
| 多重比较 | 对每个比较使用 t.Error,继续测试 |
| Goroutine 失败 | 仅使用 t.Error,切勿使用 t.Fatal |
| 测试辅助函数 | 首先调用 t.Helper() |
| 大型测试数据 | 使用带有子测试的表驱动测试 |
go-style-corego-naminggo-error-handlinggo-linting每周安装量
150
代码仓库
GitHub Stars
34
首次出现
2026年1月27日
安全审计
安装于
github-copilot140
gemini-cli138
codex138
cursor138
opencode138
kimi-cli136
Guidelines for writing clear, maintainable Go tests following Google's style.
Normative : Test failures must be diagnosable without reading the test source.
Every failure message should include:
Use the standard format: YourFunc(%v) = %v, want %v
// Good:
if got := Add(2, 3); got != 5 {
t.Errorf("Add(2, 3) = %d, want %d", got, 5)
}
// Bad: Missing function name and inputs
if got := Add(2, 3); got != 5 {
t.Errorf("got %d, want %d", got, 5)
}
Always print actual result before expected:
// Good:
t.Errorf("Parse(%q) = %v, want %v", input, got, want)
// Bad: want/got reversed
t.Errorf("Parse(%q) want %v, got %v", input, want, got)
Normative : Do not create or use assertion libraries.
Assertion libraries fragment the developer experience and often produce unhelpful failure messages.
// Bad:
assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
// Good: Use cmp package and standard comparisons
want := BlogPost{
Type: "blogPost",
Comments: 2,
Body: "Hello, world!",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetPost() mismatch (-want +got):\n%s", diff)
}
For domain-specific comparisons, return values or errors instead of calling t.Error:
// Good: Return value for use in failure message
func postLength(p BlogPost) int { return len(p.Body) }
func TestBlogPost(t *testing.T) {
post := BlogPost{Body: "Hello"}
if got, want := postLength(post), 5; got != want {
t.Errorf("postLength(post) = %v, want %v", got, want)
}
}
Advisory : Prefer
cmp.Equalandcmp.Difffor complex types.
// Good: Full struct comparison with diff - always include direction key
want := &Doc{Type: "blogPost", Authors: []string{"isaac", "albert"}}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("AddPost() mismatch (-want +got):\n%s", diff)
}
// Good: Protocol buffers
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("Foo() mismatch (-want +got):\n%s", diff)
}
Avoid unstable comparisons - don't compare JSON/serialized output that may change. Compare semantically instead.
Normative : Use
t.Errorto keep tests going; uset.Fatalonly when continuing is impossible.
Tests should report all failures in a single run:
// Good: Report all mismatches
if diff := cmp.Diff(wantMean, gotMean); diff != "" {
t.Errorf("Mean mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(wantVariance, gotVariance); diff != "" {
t.Errorf("Variance mismatch (-want +got):\n%s", diff)
}
Use t.Fatal when subsequent tests would be meaningless:
// Good: Fatal on setup failure or when continuation is pointless
gotEncoded := Encode(input)
if gotEncoded != wantEncoded {
t.Fatalf("Encode(%q) = %q, want %q", input, gotEncoded, wantEncoded)
// Decoding unexpected output is meaningless
}
gotDecoded, err := Decode(gotEncoded)
if err != nil {
t.Fatalf("Decode(%q) error: %v", gotEncoded, err)
}
Normative : Never call
t.Fatal,t.Fatalf, ort.FailNowfrom a goroutine other than the test goroutine. Uset.Errorinstead and let the test continue.
Advisory : Use table-driven tests when many cases share similar logic.
// Good:
func TestCompare(t *testing.T) {
tests := []struct {
a, b string
want int
}{
{"", "", 0},
{"a", "", 1},
{"", "a", -1},
{"abc", "abc", 0},
}
for _, tt := range tests {
got := Compare(tt.a, tt.b)
if got != tt.want {
t.Errorf("Compare(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
}
}
}
Use field names when test cases span many lines or have adjacent fields of the same type.
Don't identify rows by index - include inputs in failure messages instead of Case #%d failed.
Source : Uber Go Style Guide
When test cases need complex setup, conditional mocking, or multiple branches, prefer separate test functions over table tests.
// Bad: Too many conditional fields make tests hard to understand
tests := []struct {
give string
want string
wantErr error
shouldCallX bool // Conditional logic flag
shouldCallY bool // Another conditional flag
giveXResponse string
giveXErr error
giveYResponse string
giveYErr error
}{...}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
if tt.shouldCallX { // Conditional mock setup
xMock.EXPECT().Call().Return(tt.giveXResponse, tt.giveXErr)
}
if tt.shouldCallY { // More branching
yMock.EXPECT().Call().Return(tt.giveYResponse, tt.giveYErr)
}
// ...
})
}
// Good: Separate focused tests are clearer
func TestShouldCallX(t *testing.T) {
xMock.EXPECT().Call().Return("XResponse", nil)
got, err := DoComplexThing("inputX", xMock, yMock)
// assert...
}
func TestShouldCallYAndFail(t *testing.T) {
yMock.EXPECT().Call().Return("YResponse", nil)
_, err := DoComplexThing("inputY", xMock, yMock)
// assert error...
}
Table tests work best when:
A single shouldErr field for success/failure is acceptable if the test body is short and straightforward.
Advisory : Use subtests for better organization, filtering, and parallel execution.
Use clear, concise names: t.Run("empty_input", ...), t.Run("hu_to_en", ...)
Avoid wordy descriptions or slashes (slashes break test filtering)
Subtests must be independent - no shared state or execution order dependencies
// Good: Table tests with subtests func TestTranslate(t *testing.T) { tests := []struct { name, srcLang, dstLang, input, want string }{ {"hu_en_basic", "hu", "en", "köszönöm", "thank you"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Translate(tt.srcLang, tt.dstLang, tt.input); got != tt.want { t.Errorf("Translate(%q, %q, %q) = %q, want %q", tt.srcLang, tt.dstLang, tt.input, got, tt.want) } }) } }
Source : Uber Go Style Guide
When using t.Parallel() in table tests, be aware of loop variable capture:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Go 1.22+: tt is correctly captured per iteration
// Go 1.21-: add "tt := tt" here to capture the variable
got := Process(tt.give)
if got != tt.want {
t.Errorf("Process(%q) = %q, want %q", tt.give, got, tt.want)
}
})
}
Normative : Test helpers must call
t.Helper()and should uset.Fatalfor setup failures.
// Good: Complete test helper pattern
func mustLoadTestData(t *testing.T, filename string) []byte {
t.Helper() // Makes failures point to caller
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("Setup failed: could not read %s: %v", filename, err)
}
return data
}
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Could not open database: %v", err)
}
t.Cleanup(func() { db.Close() }) // Use t.Cleanup for teardown
return db
}
Key rules:
t.Helper() first to attribute failures to the callert.Fatal for setup failures (don't return errors)t.Cleanup() for teardown instead of deferAdvisory : Follow consistent naming for test doubles (stubs, fakes, mocks, spies).
Package naming : Append test to the production package (e.g., creditcardtest).
// Good: In package creditcardtest
// Single double - use simple name
type Stub struct{}
func (Stub) Charge(*creditcard.Card, money.Money) error { return nil }
// Multiple behaviors - name by behavior
type AlwaysCharges struct{}
type AlwaysDeclines struct{}
// Multiple types - include type name
type StubService struct{}
type StubStoredValue struct{}
Local variables : Prefix test double variables for clarity (spyCC not cc).
| Package Declaration | Use Case |
|---|---|
package foo | Same-package tests, can access unexported identifiers |
package foo_test | Black-box tests, avoids circular dependencies |
Both go in foo_test.go files. Use _test suffix when testing only public API or to break import cycles.
Advisory : Test error semantics, not error message strings.
// Bad: Brittle string comparison
if err.Error() != "invalid input" {
t.Errorf("unexpected error: %v", err)
}
// Good: Test semantic error
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("got error %v, want ErrInvalidInput", err)
}
// Good: Simple presence check when semantics don't matter
if gotErr := err != nil; gotErr != tt.wantErr {
t.Errorf("f(%v) error = %v, want error presence = %t", tt.input, err, tt.wantErr)
}
Advisory : Keep setup scoped to tests that need it.
// Good: Explicit setup in tests that need it
func TestParseData(t *testing.T) {
data := mustLoadDataset(t)
// ...
}
func TestUnrelated(t *testing.T) {
// Doesn't pay for dataset loading
}
// Bad: Global init loads data for all tests
var dataset []byte
func init() {
dataset = mustLoadDataset() // Runs even for unrelated tests
}
| Situation | Approach |
|---|---|
| Compare structs/slices | cmp.Diff(want, got) |
| Simple value mismatch | t.Errorf("F(%v) = %v, want %v", in, got, want) |
| Setup failure | t.Fatalf("Setup: %v", err) |
| Multiple comparisons | t.Error for each, continue testing |
| Goroutine failures | t.Error only, never t.Fatal |
| Test helper |
go-style-corego-naminggo-error-handlinggo-lintingWeekly Installs
150
Repository
GitHub Stars
34
First Seen
Jan 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot140
gemini-cli138
codex138
cursor138
opencode138
kimi-cli136
Spring Boot工程师技能指南:微服务架构、安全加固与云原生开发实战
2,800 周安装
竞争对手研究指南:SEO、内容、反向链接与定价分析工具
231 周安装
Azure 工作负载自动升级评估工具 - 支持 Functions、App Service 计划与 SKU 迁移
231 周安装
Kaizen持续改进方法论:软件开发中的渐进式优化与防错设计实践指南
231 周安装
软件UI/UX设计指南:以用户为中心的设计原则、WCAG可访问性与平台规范
231 周安装
Apify 网络爬虫和自动化平台 - 无需编码抓取亚马逊、谷歌、领英等网站数据
231 周安装
llama.cpp 中文指南:纯 C/C++ LLM 推理,CPU/非 NVIDIA 硬件优化部署
231 周安装
Call t.Helper() first |
| Large test data | Table-driven with subtests |