go-testing

安装量: 241
排名: #3621

安装

npx skills add https://github.com/cxuu/golang-skills --skill go-testing

Go Testing

Guidelines for writing clear, maintainable Go tests following Google's style.

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) Failure Message Format

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

Got Before Want

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)

No Assertion Libraries

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

Comparisons and Diffs

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

t.Error vs t.Fatal

Normative: Use t.Error to keep tests going; use t.Fatal only when continuing is impossible.

Keep Going

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

When to Use t.Fatal

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

Don't Call t.Fatal from Goroutines

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.

Table-Driven Tests

Advisory: Use table-driven tests when many cases share similar logic.

Basic Structure // 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.

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.

// 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.

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 // 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:

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

Test Helpers

Normative: Test helpers must call t.Helper() and should use t.Fatal for 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:

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 Test Doubles

Advisory: 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).

Test Packages 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.

Test Error Semantics

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

Setup Scoping

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 }

Quick Reference 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 Call t.Helper() first Large test data Table-driven with subtests 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

返回排行榜