Loading...
Loading...
Go testing patterns from Google and Uber style guides including test naming, table-driven tests, subtests, parallel tests, test helpers, test doubles, and assertions. Use when writing or reviewing Go test code, creating test helpers, or setting up table-driven tests.
npx skill4agent add cxuu/golang-skills go-testingNormative: Test failures must be diagnosable without reading the test source.
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)
}// 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.
// 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// 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: Preferandcmp.Equalfor complex types.cmp.Diff
// 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)
}Normative: Useto keep tests going; uset.Erroronly when continuing is impossible.t.Fatal
// 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)
}t.Fatal// 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, ort.Fatalffrom a goroutine other than the test goroutine. Uset.FailNowinstead and let the test continue.t.Error
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)
}
}
}Case #%d failedSource: Uber Go Style Guide
// 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...
}shouldErrAdvisory: Use subtests for better organization, filtering, and parallel execution.
t.Run("empty_input", ...)t.Run("hu_to_en", ...)// 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
t.Parallel()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 calland should uset.Helper()for setup failures.t.Fatal
// 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
}t.Helper()t.Fatalt.Cleanup()Advisory: Follow consistent naming for test doubles (stubs, fakes, mocks, spies).
testcreditcardtest// 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{}spyCCcc| Package Declaration | Use Case |
|---|---|
| Same-package tests, can access unexported identifiers |
| Black-box tests, avoids circular dependencies |
foo_test.go_testAdvisory: 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 | |
| Simple value mismatch | |
| Setup failure | |
| Multiple comparisons | |
| Goroutine failures | |
| Test helper | Call |
| Large test data | Table-driven with subtests |
go-style-corego-naminggo-error-handlinggo-linting