go-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Testing

Go 测试

Guidelines for writing clear, maintainable Go tests following Google's style.
遵循Google风格编写清晰、可维护的Go测试的指南。

Useful Test Failures

有价值的测试失败信息

Normative: Test failures must be diagnosable without reading the test source.
Every failure message should include:
  • What caused the failure
  • The function inputs
  • The actual result (got)
  • The expected result (want)
规范性要求:测试失败信息必须无需查看测试源码即可诊断问题。
每条失败信息应包含:
  • 失败原因
  • 函数输入参数
  • 实际结果(got)
  • 预期结果(want)

Failure Message Format

失败信息格式

Use the standard format:
YourFunc(%v) = %v, want %v
go
// 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)
}
使用标准格式:
YourFunc(%v) = %v, want %v
go
// 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)
}

Got Before Want

先显示实际结果,再显示预期结果

Always print actual result before expected:
go
// 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)

始终先打印实际结果,再打印预期结果:
go
// 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)

No Assertion Libraries

禁止使用断言库

Normative: Do not create or use assertion libraries.
Assertion libraries fragment the developer experience and often produce unhelpful failure messages.
go
// 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
:
go
// 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)
    }
}

规范性要求:不要创建或使用断言库。
断言库会割裂开发者的使用体验,且通常生成的失败信息毫无帮助。
go
// 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)
}
对于领域特定的比较,返回值或错误,而非调用
t.Error
go
// 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)
    }
}

Comparisons and Diffs

比较与差异对比

Advisory: Prefer
cmp.Equal
and
cmp.Diff
for complex types.
go
// 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.

建议性要求:对于复杂类型,优先使用
cmp.Equal
cmp.Diff
go
// 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)
}
避免不稳定的比较 - 不要比较可能发生变化的JSON/序列化输出,应从语义层面进行比较。

t.Error vs t.Fatal

t.Error 与 t.Fatal

Normative: Use
t.Error
to keep tests going; use
t.Fatal
only when continuing is impossible.
规范性要求:使用
t.Error
让测试继续执行;仅当无法继续执行时才使用
t.Fatal

Keep Going

让测试继续执行

Tests should report all failures in a single run:
go
// 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)
}
测试应在单次运行中报告所有失败:
go
// 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)
}

When to Use t.Fatal

何时使用 t.Fatal

Use
t.Fatal
when subsequent tests would be meaningless:
go
// 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)
}
当后续测试失去意义时,使用
t.Fatal
go
// 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)
    // 解码不符合预期的输出毫无意义
}
gotDecoded, err := Decode(gotEncoded)
if err != nil {
    t.Fatalf("Decode(%q) error: %v", gotEncoded, err)
}

Don't Call t.Fatal from Goroutines

不要在协程中调用 t.Fatal

Normative: Never call
t.Fatal
,
t.Fatalf
, or
t.FailNow
from a goroutine other than the test goroutine. Use
t.Error
instead and let the test continue.

规范性要求:绝不要在测试协程以外的其他协程中调用
t.Fatal
t.Fatalf
t.FailNow
。应改用
t.Error
并让测试继续执行。

Table-Driven Tests

表驱动测试

Advisory: Use table-driven tests when many cases share similar logic.
建议性要求:当多个测试用例共享相似逻辑时,使用表驱动测试。

Basic Structure

基本结构

go
// 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)
        }
    }
}
go
// 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)
        }
    }
}

Best Practices

最佳实践

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
.
使用字段名:当测试用例跨越多行或存在相邻同类型字段时,应使用字段名。
不要通过索引标识测试用例 - 在失败信息中包含输入参数,而非使用
Case #%d failed

Avoid Complexity in Table Tests

避免表驱动测试过于复杂

Source: Uber Go Style Guide
When test cases need complex setup, conditional mocking, or multiple branches, prefer separate test functions over table tests.
go
// 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:
  • All cases run identical logic (no conditional assertions)
  • Setup is the same for all cases
  • No conditional mocking based on test case fields
  • All table fields are used in all tests
A single
shouldErr
field for success/failure is acceptable if the test body is short and straightforward.

来源:Uber Go 风格指南
当测试用例需要复杂的初始化、条件模拟或多分支逻辑时,优先使用独立的测试函数,而非表驱动测试。
go
// 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...
}
表驱动测试最适合以下场景
  • 所有用例执行完全相同的逻辑(无条件断言)
  • 所有用例的初始化流程一致
  • 无需基于测试用例字段进行条件模拟
  • 所有表字段在所有测试中都会被使用
如果测试主体简短直接,仅包含一个
shouldErr
字段来标识成功/失败是可以接受的。

Subtests

子测试

Advisory: Use subtests for better organization, filtering, and parallel execution.
建议性要求:使用子测试实现更好的组织、过滤和并行执行。

Subtest Names

子测试命名

  • 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
go
// 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)
            }
        })
    }
}
  • 使用清晰、简洁的名称:
    t.Run("empty_input", ...)
    t.Run("hu_to_en", ...)
  • 避免冗长的描述或斜杠(斜杠会破坏测试过滤功能)
  • 子测试必须独立 - 无共享状态或执行顺序依赖
go
// 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)
            }
        })
    }
}

Parallel Tests

并行测试

Source: Uber Go Style Guide
When using
t.Parallel()
in table tests, be aware of loop variable capture:
go
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)
        }
    })
}

来源:Uber Go 风格指南
在表驱动测试中使用
t.Parallel()
时,需注意循环变量捕获问题:
go
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)
        }
    })
}

Test Helpers

测试辅助函数

Normative: Test helpers must call
t.Helper()
and should use
t.Fatal
for setup failures.
go
// 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:
  • Call
    t.Helper()
    first to attribute failures to the caller
  • Use
    t.Fatal
    for setup failures (don't return errors)
  • Use
    t.Cleanup()
    for teardown instead of defer

规范性要求:测试辅助函数必须调用
t.Helper()
,且对于初始化失败应使用
t.Fatal
go
// Good: Complete test helper pattern
func mustLoadTestData(t *testing.T, filename string) []byte {
    t.Helper()  // 让失败信息指向调用者
    data, err := os.ReadFile(filename)
    if err != nil {
        t.Fatalf("初始化失败:无法读取文件 %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("无法打开数据库: %v", err)
    }
    t.Cleanup(func() { db.Close() })  // 使用 t.Cleanup 进行清理
    return db
}
核心规则
  • 首先调用
    t.Helper()
    ,将失败归因于调用者
  • 对于初始化失败使用
    t.Fatal
    (不要返回错误)
  • 使用
    t.Cleanup()
    进行清理,而非defer

Test Doubles

测试替身

Advisory: Follow consistent naming for test doubles (stubs, fakes, mocks, spies).
Package naming: Append
test
to the production package (e.g.,
creditcardtest
).
go
// 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
).

建议性要求:为测试替身(存根、伪实现、模拟对象、间谍对象)遵循一致的命名规范。
包命名:在生产包名后追加
test
(例如
creditcardtest
)。
go
// Good: In package 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
)。

Test Packages

测试包

Package DeclarationUse 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.

包声明使用场景
package foo
同包测试,可以访问未导出的标识符
package foo_test
黑盒测试,避免循环依赖
两种声明都放在
foo_test.go
文件中。当仅测试公开API或需要打破导入循环时,使用
_test
后缀的包名。

Test Error Semantics

测试错误语义

Advisory: Test error semantics, not error message strings.
go
// 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)
}

建议性要求:测试错误的语义,而非错误消息字符串。
go
// 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)
}

Setup Scoping

初始化作用域

Advisory: Keep setup scoped to tests that need it.
go
// 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
}

建议性要求:将初始化操作限定在需要它的测试范围内。
go
// Good: Explicit setup in tests that need it
func TestParseData(t *testing.T) {
    data := mustLoadDataset(t)
    // ...
}

func TestUnrelated(t *testing.T) {
    // 无需加载数据集
}

// Bad: Global init loads data for all tests
var dataset []byte

func init() {
    dataset = mustLoadDataset()  // 即使是无关测试也会执行
}

Quick Reference

快速参考

SituationApproach
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 helperCall
t.Helper()
first
Large test dataTable-driven with subtests
场景处理方式
比较结构体/切片
cmp.Diff(want, got)
简单值不匹配
t.Errorf("F(%v) = %v, want %v", in, got, want)
初始化失败
t.Fatalf("Setup: %v", err)
多轮比较为每轮比较调用
t.Error
,继续执行测试
协程中的失败仅使用
t.Error
,绝不使用
t.Fatal
测试辅助函数首先调用
t.Helper()
大量测试数据使用带子测试的表驱动测试

See Also

另请参阅

  • For core style principles:
    go-style-core
  • For naming conventions:
    go-naming
  • For error handling patterns:
    go-error-handling
  • For linter configuration:
    go-linting
  • 核心风格原则:
    go-style-core
  • 命名规范:
    go-naming
  • 错误处理模式:
    go-error-handling
  • 检查器配置:
    go-linting