Testing
Testing is built into Go's toolchain. The testing package provides everything you need for unit tests, benchmarks, and examples. In this lesson, you'll learn to write effective tests and ensure code quality.
Writing Your First Test
Code to Test
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Multiply(a, b int) int {
return a * b
}
Test File
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
func TestMultiply(t *testing.T) {
result := Multiply(4, 5)
expected := 20
if result != expected {
t.Errorf("Multiply(4, 5) = %d; want %d", result, expected)
}
}
- File name ends with
_test.go - Test functions start with
Test - Take
*testing.Tparameter - Use
t.Error()ort.Fatal()to fail
Running Tests
# Run all tests
go test
# Verbose output
go test -v
# Run specific test
go test -run TestAdd
# Run tests in all subdirectories
go test ./...
Table-Driven Tests
The idiomatic way to test multiple cases:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed signs", -2, 3, 1},
{"zeros", 0, 0, 0},
{"large numbers", 1000, 2000, 3000},
}
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; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
t.Run() for subtests.
Test Helpers and Assertions
// Helper function
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper() // Marks this as helper (better error messages)
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
func TestWithHelper(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5)
}
Testing Errors
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
// Test success case
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5.0 {
t.Errorf("got %v; want 5.0", result)
}
// Test error case
_, err = Divide(10, 0)
if err == nil {
t.Error("expected error, got nil")
}
}
Test Coverage
# Run tests with coverage
go test -cover
# Generate coverage report
go test -coverprofile=coverage.out
# View coverage in browser
go tool cover -html=coverage.out
# Coverage for all packages
go test -cover ./...
- Statement coverage - Which lines executed
- Branch coverage - Which branches taken
- Function coverage - Which functions called
- Aim for 70-80% coverage (100% not always necessary)
Mocking and Interfaces
// Interface for database
type UserStore interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
// Real implementation
type DBUserStore struct {
db *sql.DB
}
func (s *DBUserStore) GetUser(id int) (*User, error) {
// Real database query
}
// Mock for testing
type MockUserStore struct {
users map[int]*User
}
func (m *MockUserStore) GetUser(id int) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserStore) SaveUser(user *User) error {
m.users[user.ID] = user
return nil
}
// Service that uses UserStore
type UserService struct {
store UserStore
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.store.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Test with mock
func TestUserService(t *testing.T) {
mock := &MockUserStore{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := &UserService{store: mock}
name, err := service.GetUserName(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if name != "Alice" {
t.Errorf("got %s; want Alice", name)
}
}
Advanced Testing Patterns
Setup and Teardown
func TestMain(m *testing.M) {
// Setup
fmt.Println("Setting up tests...")
// Run tests
code := m.Run()
// Teardown
fmt.Println("Cleaning up...")
os.Exit(code)
}
func TestWithSetup(t *testing.T) {
// Per-test setup
setup := func() *Database {
db := &Database{}
db.Connect()
return db
}
// Per-test teardown
teardown := func(db *Database) {
db.Close()
}
db := setup()
defer teardown(db)
// Test code
}
Testing HTTP Handlers
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
}
if string(body) != "Hello, World!" {
t.Errorf("got body %s; want Hello, World!", body)
}
}
Common Mistakes
1. Not using t.Helper()
// ❌ Wrong - error points to helper function
func assertEqual(t *testing.T, got, want int) {
if got != want {
t.Errorf("got %d; want %d", got, want)
}
}
// ✅ Correct - error points to test
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Mark as helper
if got != want {
t.Errorf("got %d; want %d", got, want)
}
}
2. Testing implementation instead of behavior
// ❌ Wrong - testing internal details
func TestSort(t *testing.T) {
// Testing which sorting algorithm is used
}
// ✅ Correct - testing behavior
func TestSort(t *testing.T) {
input := []int{3, 1, 2}
Sort(input)
expected := []int{1, 2, 3}
// Test that output is sorted
}
3. Not running tests in parallel
// ❌ Slow - sequential tests
func TestSlow(t *testing.T) {
time.Sleep(time.Second)
}
// ✅ Fast - parallel tests
func TestFast(t *testing.T) {
t.Parallel() // Run in parallel
time.Sleep(time.Second)
}
Exercise: Test a Calculator
Task: Write comprehensive tests for a calculator.
Requirements:
- Test Add, Subtract, Multiply, Divide functions
- Use table-driven tests
- Test error cases (division by zero)
- Achieve >80% coverage
Show Solution
// calculator.go
package calculator
import "errors"
func Add(a, b float64) float64 {
return a + b
}
func Subtract(a, b float64) float64 {
return a - b
}
func Multiply(a, b float64) float64 {
return a * b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
}{
{"positive", 2.5, 3.5, 6.0},
{"negative", -2.5, -3.5, -6.0},
{"mixed", -2.5, 3.5, 1.0},
{"zeros", 0, 0, 0},
}
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(%.2f, %.2f) = %.2f; want %.2f",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestDivide(t *testing.T) {
// Success cases
tests := []struct {
name string
a, b float64
expected float64
}{
{"normal", 10, 2, 5},
{"decimal", 7, 2, 3.5},
{"negative", -10, 2, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("Divide(%.2f, %.2f) = %.2f; want %.2f",
tt.a, tt.b, result, tt.expected)
}
})
}
// Error case
t.Run("division by zero", func(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("expected error, got nil")
}
})
}
Summary
- Test files end with
_test.go - Test functions start with
Test - Table-driven tests are idiomatic Go
- t.Helper() improves error messages
- go test -cover shows coverage
- Interfaces enable mocking
- t.Parallel() runs tests concurrently
- Test behavior, not implementation
What's Next?
Now that you can write tests, you're ready to learn about Benchmarking. You'll discover how to measure performance, optimize code, and ensure your programs run efficiently!
Enjoying these tutorials?