Go Basic Testing — testing.T and Table-Driven Tests
Go has the testing package built into its standard library. You can set up a complete testing environment without any additional frameworks.
Characteristics of Go Testing
- Test files use the
_test.gosuffix - Test functions start with
Testand take*testing.Tas a parameter - Run with
go test ./...command - Test files are excluded from builds
Writing Basic Tests
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
func IsPrime(n int) bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
// math_test.go
package math
import (
"testing"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
// t.Errorf: record failure and continue execution
t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
}
}
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err) // stop immediately
}
if result != 5.0 {
t.Errorf("Divide(10, 2) = %f; expected %f", result, 5.0)
}
}
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("dividing by zero should return an error")
}
}
Test Execution Commands
# Test current package
go test
# Test all packages
go test ./...
# Verbose output (-v)
go test -v ./...
# Run specific tests only
go test -run TestAdd ./...
# Pattern matching
go test -run TestDivide ./...
# Measure coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # view in browser
Table-Driven Tests
The standard testing pattern in Go. Define multiple input/output pairs as a struct slice.
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"adding positives", 2, 3, 5},
{"adding negatives", -1, -2, -3},
{"positive and negative", 5, -3, 2},
{"adding to zero", 0, 5, 5},
{"large numbers", 1000000, 999999, 1999999},
}
for _, tt := range tests {
// t.Run executes each case as a subtest
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; expected %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestIsPrimeTableDriven(t *testing.T) {
tests := []struct {
n int
expected bool
}{
{1, false},
{2, true},
{3, true},
{4, false},
{17, true},
{100, false},
{97, true},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("IsPrime(%d)", tt.n), func(t *testing.T) {
result := IsPrime(tt.n)
if result != tt.expected {
t.Errorf("IsPrime(%d) = %v; expected %v", tt.n, result, tt.expected)
}
})
}
}
Table-Driven Tests with Error Cases
func TestDivideTable(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
wantError bool
}{
{"normal division", 10.0, 2.0, 5.0, false},
{"decimal result", 7.0, 2.0, 3.5, false},
{"divide by zero", 10.0, 0.0, 0.0, true},
{"negative division", -10.0, 2.0, -5.0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.wantError {
if err == nil {
t.Error("error expected but got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("Divide(%g, %g) = %g; expected %g",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Helper Function Pattern
// t.Helper() makes failure line number point to caller, not the helper
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func assertError(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatal("error expected")
}
}
// Usage example
func TestWithHelpers(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5)
_, err := Divide(10, 0)
assertError(t, err)
}
testify Library — Convenient Assertions
go get github.com/stretchr/testify/assert
go get github.com/stretchr/testify/require
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWithTestify(t *testing.T) {
// assert: continue on failure
result := Add(2, 3)
assert.Equal(t, 5, result, "Add(2, 3) should be 5")
assert.NotEqual(t, 0, result)
// require: stop on failure (when test has dependencies)
user, err := findUser(1)
require.NoError(t, err, "finding user should not error")
require.NotNil(t, user)
assert.Equal(t, "Kim Golang", user.Name)
assert.Greater(t, user.Age, 0)
// Slice/map comparison
assert.ElementsMatch(t, []int{3, 1, 2}, []int{1, 2, 3})
assert.Contains(t, "hello world", "world")
}
TestMain — Control Test Lifecycle
func TestMain(m *testing.M) {
// Setup before all tests
fmt.Println("setting up test environment...")
setupTestEnvironment()
// Run tests
code := m.Run()
// Cleanup after all tests
fmt.Println("tearing down test environment...")
teardownTestEnvironment()
os.Exit(code)
}
Parallel Tests
func TestParallel(t *testing.T) {
tests := []struct {
name string
input int
}{
{"case 1", 1},
{"case 2", 2},
{"case 3", 3},
}
for _, tt := range tests {
tt := tt // capture loop variable (required for Go < 1.22)
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // run subtests in parallel
// independent test work
result := expensiveOperation(tt.input)
assert.Greater(t, result, 0)
})
}
}
Setup & Teardown Pattern
type TestSuite struct {
db *sql.DB
repo *UserRepository
}
func setupSuite(t *testing.T) (*TestSuite, func()) {
t.Helper()
db := setupTestDB(t)
suite := &TestSuite{
db: db,
repo: NewUserRepository(db),
}
cleanup := func() {
db.Exec("TRUNCATE TABLE users CASCADE")
}
return suite, cleanup
}
func TestUserRepository(t *testing.T) {
suite, cleanup := setupSuite(t)
defer cleanup()
t.Run("create user", func(t *testing.T) {
user, err := suite.repo.Create(context.Background(), "test", "test@ex.com", 25)
require.NoError(t, err)
assert.NotZero(t, user.ID)
})
t.Run("find user", func(t *testing.T) {
// ...
})
}
Coverage Analysis
# Measure coverage
go test -coverprofile=coverage.out ./...
# View coverage by function
go tool cover -func=coverage.out
# HTML report (in browser)
go tool cover -html=coverage.out -o coverage.html
# Specific packages only
go test -cover -covermode=atomic ./internal/...
Key Summary
| Function/Method | Purpose |
|---|---|
t.Error(f) | Record failure and continue |
t.Fatal(f) | Record failure and stop |
t.Log(f) | Log output (shown only on failure) |
t.Helper() | Mark as helper function |
t.Parallel() | Declare parallel test |
t.Run(name, f) | Run subtest |
t.Skip() | Skip test |
- Use table-driven tests to minimize code duplication
- Always add
t.Helper()to helper functions - Be careful with loop variable capture in parallel tests (Go 1.22+ handles automatically)