Go Table-Driven Tests
Use this skill when writing or modifying Go table-driven tests. It ensures tests follow established patterns.
Core Principles
One test function, many cases
- Define test cases in a slice and iterate with
t.Run()
Explicit naming
- Each case has a
name
field that becomes the subtest name
Structured inputs
- Use struct fields for inputs, expected outputs, and configuration
Helper functions
- Use
t.Helper()
in test helpers for proper line reporting
Environment guards
- Skip integration tests when credentials are unavailable
Table Structure Pattern
func
TestFunctionName
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
// required: subtest name
input Type
// function input
want Type
// expected output
wantErr
error
// expected error (nil for success)
errCheck
func
(
error
)
bool
// optional: custom error validation
setupEnv
func
(
)
func
(
)
// optional: env setup, returns cleanup
}
{
{
name
:
"descriptive case name"
,
input
:
"test input"
,
want
:
"expected output"
,
}
,
// ... more cases
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
// test implementation using tt fields
}
)
}
}
Field Guidelines
Field
Required
Purpose
name
Yes
Subtest name - be descriptive and specific
input
/
args
Varies
Input values for the function under test
want
/
want
Varies
Expected output values (e.g.,
wantErr
,
wantResult
)
errCheck
No
Custom error validation function
setupEnv
No
Environment setup function returning cleanup
Naming Conventions
Test function:
Test
or
Test_
Subtest names: lowercase, descriptive, spaces allowed
Input fields: match parameter names or use
input
/
args
Output fields: prefix with
want
(e.g.,
want
,
wantErr
,
wantResult
)
Common Patterns
1. Basic Table Test
func
TestWithRegion
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
region
string
}
{
{
"auto region"
,
"auto"
}
,
{
"us-west-2"
,
"us-west-2"
}
,
{
"eu-central-1"
,
"eu-central-1"
}
,
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
o
:=
&
Options
{
}
WithRegion
(
tt
.
region
)
(
o
)
if
o
.
Region
!=
tt
.
region
{
t
.
Errorf
(
"Region = %v, want %v"
,
o
.
Region
,
tt
.
region
)
}
}
)
}
}
2. Error Checking with
wantErr
func
TestNew_errorCases
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
input
string
wantErr
error
}
{
{
"empty input"
,
""
,
ErrInvalidInput
}
,
{
"invalid input"
,
"!!!"
,
ErrInvalidInput
}
,
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
_
,
err
:=
Parse
(
tt
.
input
)
if
!
errors
.
Is
(
err
,
tt
.
wantErr
)
{
t
.
Errorf
(
"error = %v, want %v"
,
err
,
tt
.
wantErr
)
}
}
)
}
}
3. Custom Error Validation with
errCheck
func
TestNew_customErrors
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
setupEnv
func
(
)
func
(
)
wantErr
error
errCheck
func
(
error
)
bool
}
{
{
name
:
"no bucket name returns ErrNoBucketName"
,
setupEnv
:
func
(
)
func
(
)
{
return
func
(
)
{
}
}
,
wantErr
:
ErrNoBucketName
,
errCheck
:
func
(
err
error
)
bool
{
return
errors
.
Is
(
err
,
ErrNoBucketName
)
}
,
}
,
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
cleanup
:=
tt
.
setupEnv
(
)
defer
cleanup
(
)
_
,
err
:=
New
(
context
.
Background
(
)
)
if
tt
.
wantErr
!=
nil
{
if
tt
.
errCheck
!=
nil
{
if
!
tt
.
errCheck
(
err
)
{
t
.
Errorf
(
"error = %v, want %v"
,
err
,
tt
.
wantErr
)
}
}
}
}
)
}
}
4. Environment Setup with
setupEnv
func
TestNew_envVarOverrides
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
setupEnv
func
(
)
func
(
)
options
[
]
Option
wantErr
error
}
{
{
name
:
"bucket from env var"
,
setupEnv
:
func
(
)
func
(
)
{
os
.
Setenv
(
"TIGRIS_STORAGE_BUCKET"
,
"test-bucket"
)
return
func
(
)
{
os
.
Unsetenv
(
"TIGRIS_STORAGE_BUCKET"
)
}
}
,
wantErr
:
nil
,
}
,
{
name
:
"bucket from option overrides env var"
,
setupEnv
:
func
(
)
func
(
)
{
os
.
Setenv
(
"TIGRIS_STORAGE_BUCKET"
,
"env-bucket"
)
return
func
(
)
{
os
.
Unsetenv
(
"TIGRIS_STORAGE_BUCKET"
)
}
}
,
options
:
[
]
Option
{
func
(
o
*
Options
)
{
o
.
BucketName
=
"option-bucket"
}
,
}
,
wantErr
:
nil
,
}
,
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
cleanup
:=
tt
.
setupEnv
(
)
defer
cleanup
(
)
_
,
err
:=
New
(
context
.
Background
(
)
,
tt
.
options
...
)
if
tt
.
wantErr
!=
nil
&&
!
errors
.
Is
(
err
,
tt
.
wantErr
)
{
t
.
Errorf
(
"error = %v, want %v"
,
err
,
tt
.
wantErr
)
}
}
)
}
}
Integration Test Guards
For tests requiring real credentials, use a skip helper:
// skipIfNoCreds skips the test if Tigris credentials are not set.
// Use this for integration tests that require real Tigris operations.
func
skipIfNoCreds
(
t
*
testing
.
T
)
{
t
.
Helper
(
)
if
os
.
Getenv
(
"TIGRIS_STORAGE_ACCESS_KEY_ID"
)
==
""
||
os
.
Getenv
(
"TIGRIS_STORAGE_SECRET_ACCESS_KEY"
)
==
""
{
t
.
Skip
(
"skipping: TIGRIS_STORAGE_ACCESS_KEY_ID and TIGRIS_STORAGE_SECRET_ACCESS_KEY not set"
)
}
}
func
TestCreateBucket
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
bucket
string
options
[
]
BucketOption
wantErr
error
}
{
{
name
:
"create snapshot-enabled bucket"
,
bucket
:
"test-bucket"
,
options
:
[
]
BucketOption
{
WithEnableSnapshot
(
)
}
,
}
,
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
skipIfNoCreds
(
t
)
// test implementation
}
)
}
}
Test Helpers
Use
t.Helper()
in helper functions for proper line number reporting:
func
setupTestBucket
(
t
*
testing
.
T
,
ctx context
.
Context
,
client
*
Client
)
string
{
t
.
Helper
(
)
skipIfNoCreds
(
t
)
bucket
:=
"test-bucket-"
+
randomSuffix
(
)
err
:=
client
.
CreateBucket
(
ctx
,
bucket
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to create test bucket: %v"
,
err
)
}
return
bucket
}
func
cleanupTestBucket
(
t
*
testing
.
T
,
ctx context
.
Context
,
client
*
Client
,
bucket
string
)
{
t
.
Helper
(
)
err
:=
client
.
DeleteBucket
(
ctx
,
bucket
,
WithForceDelete
(
)
)
if
err
!=
nil
{
t
.
Logf
(
"warning: failed to cleanup test bucket %s: %v"
,
bucket
,
err
)
}
}
Checklist
When writing table-driven tests:
Table struct has
name
field as first field
Each test case has a descriptive name
Input fields use clear naming (match parameters or use
input
)
Expected output fields prefixed with
want
Iteration uses
t.Run(tt.name, func(t testing.T) { ... })
Error checking uses
errors.Is()
for error comparison
Environment setup includes cleanup in
defer
Integration tests use
skipIfNoCreds(t)
helper
Test helpers use
t.Helper()
for proper line reporting
Test file is
*_test.go
and lives next to the code it tests
Best Practices
Detailed Error Messages
Include both actual and expected values in error messages for clear failure diagnosis:
t
.
Errorf
(
"got %q, want %q"
,
actual
,
expected
)
Note:
t.Errorf
is not an assertion - the test continues after logging. This helps identify whether failures are systematic or isolated to specific cases.
Maps for Test Cases
Consider using a map instead of a slice for test cases. Map iteration order is non-deterministic, which ensures test cases are truly independent:
tests
:=
map
[
string
]
struct
{
input
string
want
string
}
{
"empty string"
:
{
input
:
""
,
want
:
""
}
,
"single character"
:
{
input
:
"x"
,
want
:
"x"
}
,
"multi-byte glyph"
:
{
input
:
"🎉"
,
want
:
"🎉"
}
,
}
for
name
,
tt
:=
range
tests
{
t
.
Run
(
name
,
func
(
t
*
testing
.
T
)
{
got
:=
process
(
tt
.
input
)
if
got
!=
tt
.
want
{
t
.
Errorf
(
"got %q, want %q"
,
got
,
tt
.
want
)
}
}
)
}
Parallel Testing
Add
t.Parallel()
calls to run test cases in parallel. The loop variable is automatically captured per iteration:
func
TestFunction
(
t
*
testing
.
T
)
{
tests
:=
[
]
struct
{
name
string
input
string
}
{
// ... test cases
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Parallel
(
)
// marks this subtest as parallel
// test implementation
}
)
}
}
References
Go Wiki: TableDrivenTests
- Official Go community best practices for table-driven testing
Go Testing Package
- Standard library testing documentation
Prefer Table Driven Tests
- Dave Cheney's guide on when and why to use table-driven tests over traditional test structures