Testing in Go
Go has excellent built-in testing support. No external frameworks needed.
Basic Testing
Test File Naming
- Test files end with
_test.go - Test files can be in the same package or
package_test(black-box testing)
calculator.go # Implementation
calculator_test.go # Tests
Writing Tests
// calculator.go
package calculator
func Add(a, b int) int {
return a + b
}
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; expected %d", result, expected)
}
}
Running Tests
# Run all tests in current directory
go test
# Run all tests in project
go test ./...
# Verbose output
go test -v
# Run specific test
go test -run TestAdd
# Run tests matching pattern
go test -run "TestAdd|TestSubtract"
# Run tests in specific package
go test ./pkg/calculator
# With coverage
go test -cover
# Generate coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Testing Package (testing.T)
Common Methods
func TestExample(t *testing.T) {
// Log message (only shown with -v or on failure)
t.Log("This is a log message")
t.Logf("Formatted: %d", 42)
// Fail test but continue
t.Error("Test failed")
t.Errorf("Test failed: %v", err)
// Fail test and stop immediately
t.Fatal("Critical failure")
t.Fatalf("Critical failure: %v", err)
// Mark test as failed (without message)
t.Fail() // Continue running
t.FailNow() // Stop immediately
// Skip test
t.Skip("Skipping because...")
t.Skipf("Skipping: %s", reason)
// Check if failed
if t.Failed() {
// ...
}
}
Table-Driven Tests
The most common Go testing pattern:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
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)
}
})
}
}
Benefits of Table-Driven Tests
- Easy to add new test cases
- Clear test documentation
- Subtests can be run individually:
go test -run TestAdd/positive
Subtests
Use t.Run() for subtests:
func TestUser(t *testing.T) {
t.Run("Create", func(t *testing.T) {
// Test creation
})
t.Run("Update", func(t *testing.T) {
// Test update
})
t.Run("Delete", func(t *testing.T) {
// Test deletion
})
}
Parallel Subtests
func TestParallel(t *testing.T) {
tests := []struct {
name string
// ...
}{
{"test1"},
{"test2"},
{"test3"},
}
for _, tt := range tests {
tt := tt // Capture range variable (not needed in Go 1.22+)
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run in parallel
// Test code...
})
}
}
Setup and Teardown
Per-Test
func TestSomething(t *testing.T) {
// Setup
db := setupDatabase()
// Teardown (using defer)
defer db.Close()
// Test...
}
Using t.Cleanup
func TestSomething(t *testing.T) {
db := setupDatabase()
t.Cleanup(func() {
db.Close() // Runs after test completes
})
// Test...
}
TestMain (Package-Level Setup)
func TestMain(m *testing.M) {
// Setup before all tests
setup()
// Run tests
code := m.Run()
// Teardown after all tests
teardown()
os.Exit(code)
}
func setup() {
// Initialize test database, etc.
}
func teardown() {
// Clean up
}
Test Helpers
Helper Functions
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper() // Marks this as a helper (better error reporting)
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, Add(2, 3), 5) // Error points to this line, not helper
}
Testing Errors
func TestDivide(t *testing.T) {
t.Run("valid division", func(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5 {
t.Errorf("got %f; want 5", result)
}
})
t.Run("division by zero", func(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("expected error for division by zero")
}
})
}
Mocking and Interfaces
Use interfaces for testability:
// Production code
type EmailSender interface {
Send(to, subject, body string) error
}
type UserService struct {
emailer EmailSender
}
func (s *UserService) Register(email string) error {
// Create user...
return s.emailer.Send(email, "Welcome!", "Thanks for joining")
}
// Test code
type MockEmailer struct {
SendCalled bool
LastTo string
}
func (m *MockEmailer) Send(to, subject, body string) error {
m.SendCalled = true
m.LastTo = to
return nil
}
func TestRegister(t *testing.T) {
mock := &MockEmailer{}
service := &UserService{emailer: mock}
err := service.Register("test@example.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.SendCalled {
t.Error("expected email to be sent")
}
if mock.LastTo != "test@example.com" {
t.Errorf("sent to %s; want test@example.com", mock.LastTo)
}
}
HTTP Testing
Testing Handlers
import (
"net/http"
"net/http/httptest"
"testing"
)
func HelloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
func TestHelloHandler(t *testing.T) {
// Create request
req := httptest.NewRequest("GET", "/hello", nil)
// Create response recorder
rr := httptest.NewRecorder()
// Call handler
HelloHandler(rr, req)
// Check status code
if rr.Code != http.StatusOK {
t.Errorf("status = %d; want %d", rr.Code, http.StatusOK)
}
// Check body
if rr.Body.String() != "Hello, World!" {
t.Errorf("body = %q; want %q", rr.Body.String(), "Hello, World!")
}
}
Test Server
func TestAPIClient(t *testing.T) {
// Create test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "ok"}`))
}))
defer server.Close()
// Use server.URL in your client
client := NewAPIClient(server.URL)
resp, err := client.GetStatus()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.Status != "ok" {
t.Errorf("status = %s; want ok", resp.Status)
}
}
Benchmarks
Measure performance:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// With setup
func BenchmarkProcess(b *testing.B) {
data := generateTestData()
b.ResetTimer() // Don't count setup time
for i := 0; i < b.N; i++ {
Process(data)
}
}
// With different inputs
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := generateData(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sort(data)
}
})
}
}
Running Benchmarks
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkAdd
# With memory stats
go test -bench=. -benchmem
# Multiple iterations for stability
go test -bench=. -count=5
Fuzzing (Go 1.18+)
Automatically generate test inputs:
func FuzzReverse(f *testing.F) {
// Seed corpus
f.Add("hello")
f.Add("world")
f.Add("")
f.Fuzz(func(t *testing.T, s string) {
reversed := Reverse(s)
doubleReversed := Reverse(reversed)
if s != doubleReversed {
t.Errorf("double reverse of %q is %q", s, doubleReversed)
}
})
}
# Run fuzz test
go test -fuzz=FuzzReverse
# Limit time
go test -fuzz=FuzzReverse -fuzztime=30s
Examples as Tests
Examples serve as documentation AND tests:
func ExampleAdd() {
result := Add(2, 3)
fmt.Println(result)
// Output: 5
}
func ExampleUser_Name() {
u := User{FirstName: "John", LastName: "Doe"}
fmt.Println(u.Name())
// Output: John Doe
}
// Unordered output
func ExampleShuffle() {
fmt.Println(Shuffle([]int{1, 2, 3}))
// Unordered output:
// 1
// 2
// 3
}
Testing Best Practices
Test Naming
// Function: TestXxx
// Method: TestType_Method
// Subtests: descriptive names
func TestUser(t *testing.T) {}
func TestUser_Validate(t *testing.T) {}
t.Run("returns error for empty email", func(t *testing.T) {})
Test Independence
// BAD: tests depend on order
var counter = 0
func TestA(t *testing.T) {
counter = 5
}
func TestB(t *testing.T) {
// Depends on TestA running first!
if counter != 5 { ... }
}
// GOOD: each test sets up its own state
func TestA(t *testing.T) {
counter := 5
// ...
}
Golden Files
For complex output comparison:
func TestGenerateReport(t *testing.T) {
result := GenerateReport(testData)
golden := filepath.Join("testdata", "report.golden")
if *update {
os.WriteFile(golden, result, 0644)
}
expected, _ := os.ReadFile(golden)
if !bytes.Equal(result, expected) {
t.Errorf("output mismatch")
}
}
// Run with: go test -update
var update = flag.Bool("update", false, "update golden files")
Test Coverage
# Show coverage percentage
go test -cover
# Generate coverage profile
go test -coverprofile=coverage.out
# View in browser
go tool cover -html=coverage.out
# Coverage by function
go tool cover -func=coverage.out
# Coverage for specific packages
go test -cover ./pkg/...
Next Steps
Continue to 12-standard-library.md to learn about Go's standard library.