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