Testing Fundamentals

When building a bridge, you wouldn't wait until the entire structure is complete before testing if it can hold weight. Instead, you'd test each component - the cables, the supports, the deck - individually and then test how they work together. This is exactly what software testing is about: verifying that individual components work correctly in isolation and that they integrate properly with the rest of the system.

Testing is the cornerstone of reliable software development. This comprehensive guide covers testing fundamentals in Go, from unit tests and table-driven tests to mocks, benchmarks, and integration testing patterns that enable you to build production-ready applications with confidence.

πŸ’‘ Key Takeaway: Good tests are not just about finding bugs - they're about documenting behavior, enabling refactoring, and giving you confidence to ship code. The best tests are fast, focused, and make failures obvious.

Testing Philosophy

Think of tests like a safety net for a trapeze artist. The net doesn't prevent mistakes - the artist still needs skill and practice. But it gives them the confidence to attempt complex maneuvers, knowing that if something goes wrong, they won't fall to the ground.

Tests provide confidence to change code without fear.

Why Testing Matters:

  • Early Bug Detection: Catch issues before they reach production
  • Documentation: Tests describe how code should behave
  • Refactoring Confidence: Change code knowing tests will catch regressions
  • Design Feedback: Hard-to-test code often indicates design problems
  • Regression Prevention: Ensure fixed bugs stay fixed
  • Living Specification: Tests document expected behavior better than comments

Testing Pyramid:

  • Unit Tests (70%): Fast, focused tests of individual functions/methods
  • Integration Tests (20%): Test interactions between components
  • End-to-End Tests (10%): Test complete user workflows

⚠️ Important: More tests doesn't always mean better testing. Aim for tests that provide value, catch real bugs, and don't slow down development. Quality over quantity.

Real-world Example: Google maintains over 2 billion lines of code with hundreds of thousands of tests. Their testing philosophy focuses on fast feedback loops - most tests complete in seconds, enabling developers to iterate quickly while maintaining quality.

Test-Driven Development (TDD)

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6)
 7
 8// TDD Cycle: Red -> Green -> Refactor
 9
10// 1. RED: Write a failing test
11func TestCalculateTotal_ShouldSumItems(t *testing.T) {
12	items := []float64{10.0, 20.0, 30.0}
13	expected := 60.0
14
15	result := CalculateTotal(items)
16
17	if result != expected {
18		t.Errorf("Expected %.2f, got %.2f", expected, result)
19	}
20}
21
22// 2. GREEN: Write minimal code to pass
23func CalculateTotal(items []float64) float64 {
24	var total float64
25	for _, item := range items {
26		total += item
27	}
28	return total
29}
30
31// 3. REFACTOR: Improve the code while keeping tests green
32
33func main() {
34	fmt.Println("TDD Cycle Demonstration")
35	fmt.Println("=======================\n")
36
37	fmt.Println("Red-Green-Refactor Cycle:")
38	fmt.Println("1. RED: Write a failing test first")
39	fmt.Println("2. GREEN: Write minimal code to make it pass")
40	fmt.Println("3. REFACTOR: Improve code quality while keeping tests green")
41
42	fmt.Println("\nBenefits of TDD:")
43	fmt.Println("- Forces you to think about requirements first")
44	fmt.Println("- Creates comprehensive test coverage naturally")
45	fmt.Println("- Results in testable, well-designed code")
46	fmt.Println("- Provides immediate feedback on changes")
47	fmt.Println("- Acts as executable documentation")
48
49	// Demonstrate the function
50	items := []float64{10.0, 20.0, 30.0}
51	total := CalculateTotal(items)
52	fmt.Printf("\nCalculateTotal(%v) = %.2f\n", items, total)
53}

Unit Testing Basics

Unit tests are like checking individual ingredients before cooking. You test each component in isolation to ensure it works correctly before combining them into a larger system.

Unit tests verify individual functions work correctly in isolation.

Anatomy of a Good Unit Test:

  • Arrange: Set up test data and conditions
  • Act: Execute the function being tested
  • Assert: Verify the results match expectations
  • Cleanup: Clean up any resources (defer statements)

Writing Your First Test

 1package calculator
 2
 3import (
 4	"errors"
 5	"testing"
 6)
 7
 8// calculator.go
 9func Add(a, b int) int {
10	return a + b
11}
12
13func Multiply(a, b int) int {
14	return a * b
15}
16
17func Divide(a, b float64) (float64, error) {
18	if b == 0 {
19		return 0, errors.New("division by zero")
20	}
21	return a / b, nil
22}
23
24// calculator_test.go
25func TestAdd(t *testing.T) {
26	result := Add(2, 3)
27	expected := 5
28
29	if result != expected {
30		t.Errorf("Add(2, 3) = %d; want %d", result, expected)
31	}
32}
33
34func TestMultiply(t *testing.T) {
35	result := Multiply(3, 4)
36	expected := 12
37
38	if result != expected {
39		t.Errorf("Multiply(3, 4) = %d; want %d", result, expected)
40	}
41}
42
43func TestDivide(t *testing.T) {
44	result, err := Divide(10, 2)
45
46	if err != nil {
47		t.Fatalf("Unexpected error: %v", err)
48	}
49
50	expected := 5.0
51	if result != expected {
52		t.Errorf("Divide(10, 2) = %.2f; want %.2f", result, expected)
53	}
54}
55
56func TestDivide_ByZero(t *testing.T) {
57	_, err := Divide(10, 0)
58
59	if err == nil {
60		t.Error("Expected error for division by zero, got nil")
61	}
62}

Testing Best Practices

  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"strings"
  7	"testing"
  8)
  9
 10// Good: Clear, descriptive test names
 11func TestUserValidation_WithEmptyEmail_ReturnsError(t *testing.T) {
 12	// Test implementation
 13}
 14
 15// Bad: Vague test name
 16func TestUser(t *testing.T) {
 17	// What aspect of user?
 18}
 19
 20// Good: Use subtests for related scenarios
 21func TestEmailValidation(t *testing.T) {
 22	t.Run("valid email", func(t *testing.T) {
 23		valid := ValidateEmail("test@example.com")
 24		if !valid {
 25			t.Error("Expected valid email")
 26		}
 27	})
 28
 29	t.Run("missing @ symbol", func(t *testing.T) {
 30		valid := ValidateEmail("testexample.com")
 31		if valid {
 32			t.Error("Expected invalid email")
 33		}
 34	})
 35
 36	t.Run("missing domain", func(t *testing.T) {
 37		valid := ValidateEmail("test@")
 38		if valid {
 39			t.Error("Expected invalid email")
 40		}
 41	})
 42}
 43
 44func ValidateEmail(email string) bool {
 45	return strings.Contains(email, "@") &&
 46	       strings.Contains(email, ".") &&
 47	       len(email) >= 5
 48}
 49
 50// Good: Test both success and failure cases
 51func TestParseInt_ValidInput(t *testing.T) {
 52	// Test happy path
 53}
 54
 55func TestParseInt_InvalidInput(t *testing.T) {
 56	// Test error handling
 57}
 58
 59// Good: Use helper functions to reduce duplication
 60func assertEqual(t *testing.T, got, want interface{}) {
 61	t.Helper() // Mark as helper function
 62	if got != want {
 63		t.Errorf("got %v, want %v", got, want)
 64	}
 65}
 66
 67func TestWithHelper(t *testing.T) {
 68	result := Add(2, 3)
 69	assertEqual(t, result, 5)
 70}
 71
 72func Add(a, b int) int {
 73	return a + b
 74}
 75
 76func main() {
 77	fmt.Println("Testing Best Practices")
 78	fmt.Println("=====================\n")
 79
 80	fmt.Println("1. Clear Test Names:")
 81	fmt.Println("   - Use descriptive names that explain what's being tested")
 82	fmt.Println("   - Include expected behavior and conditions")
 83	fmt.Println("   - Format: TestFunction_Condition_ExpectedBehavior")
 84
 85	fmt.Println("\n2. Arrange-Act-Assert Pattern:")
 86	fmt.Println("   - Arrange: Set up test data")
 87	fmt.Println("   - Act: Execute the function")
 88	fmt.Println("   - Assert: Verify results")
 89
 90	fmt.Println("\n3. Test Independence:")
 91	fmt.Println("   - Each test should run independently")
 92	fmt.Println("   - No shared state between tests")
 93	fmt.Println("   - Tests should pass in any order")
 94
 95	fmt.Println("\n4. Use Subtests:")
 96	fmt.Println("   - Group related test cases")
 97	fmt.Println("   - Enable running specific scenarios")
 98	fmt.Println("   - Improve test organization")
 99
100	fmt.Println("\n5. Helper Functions:")
101	fmt.Println("   - Reduce test code duplication")
102	fmt.Println("   - Mark helpers with t.Helper()")
103	fmt.Println("   - Keep test logic clear and readable")
104}

Table-Driven Tests

Imagine a restaurant testing new recipes. Instead of cooking each dish separately and taking notes, they create a standardized testing form where they can quickly test multiple variations with the same evaluation criteria. This is exactly what table-driven tests do - they let you test multiple scenarios efficiently using the same test logic.

Table-driven tests enable testing multiple scenarios with minimal code duplication.

When to Use Table-Driven Tests:

  • Testing the same logic with different inputs
  • Boundary condition testing
  • Error handling with various error cases
  • Validating multiple similar scenarios
  • Regression testing with known inputs/outputs

πŸ’‘ Key Takeaway: Table-driven tests make it easy to add new test cases - just add a row to the table. This encourages thorough testing and makes your test suite more maintainable.

Basic Table-Driven Test

 1package main
 2
 3import (
 4	"strings"
 5	"testing"
 6)
 7
 8func Reverse(s string) string {
 9	runes := []rune(s)
10	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
11		runes[i], runes[j] = runes[j], runes[i]
12	}
13	return string(runes)
14}
15
16func TestReverse(t *testing.T) {
17	tests := []struct {
18		name  string
19		input string
20		want  string
21	}{
22		{
23			name:  "empty string",
24			input: "",
25			want:  "",
26		},
27		{
28			name:  "single character",
29			input: "a",
30			want:  "a",
31		},
32		{
33			name:  "simple word",
34			input: "hello",
35			want:  "olleh",
36		},
37		{
38			name:  "with spaces",
39			input: "hello world",
40			want:  "dlrow olleh",
41		},
42		{
43			name:  "with unicode",
44			input: "Hello, δΈ–η•Œ",
45			want:  "η•ŒδΈ– ,olleH",
46		},
47		{
48			name:  "palindrome",
49			input: "racecar",
50			want:  "racecar",
51		},
52	}
53
54	for _, tt := range tests {
55		t.Run(tt.name, func(t *testing.T) {
56			got := Reverse(tt.input)
57			if got != tt.want {
58				t.Errorf("Reverse(%q) = %q, want %q", tt.input, got, tt.want)
59			}
60		})
61	}
62}

Advanced Table-Driven Patterns

  1// run
  2package main
  3
  4import (
  5	"errors"
  6	"fmt"
  7	"strings"
  8	"testing"
  9)
 10
 11// Function to test
 12type User struct {
 13	Name  string
 14	Email string
 15	Age   int
 16}
 17
 18func ValidateUser(u User) error {
 19	if u.Name == "" {
 20		return errors.New("name is required")
 21	}
 22	if !strings.Contains(u.Email, "@") {
 23		return errors.New("invalid email format")
 24	}
 25	if u.Age < 0 || u.Age > 150 {
 26		return errors.New("invalid age")
 27	}
 28	return nil
 29}
 30
 31// Table-driven test with error checking
 32func TestValidateUser(t *testing.T) {
 33	tests := []struct {
 34		name    string
 35		user    User
 36		wantErr bool
 37		errMsg  string
 38	}{
 39		{
 40			name: "valid user",
 41			user: User{
 42				Name:  "John Doe",
 43				Email: "john@example.com",
 44				Age:   30,
 45			},
 46			wantErr: false,
 47		},
 48		{
 49			name: "missing name",
 50			user: User{
 51				Name:  "",
 52				Email: "john@example.com",
 53				Age:   30,
 54			},
 55			wantErr: true,
 56			errMsg:  "name is required",
 57		},
 58		{
 59			name: "invalid email",
 60			user: User{
 61				Name:  "John Doe",
 62				Email: "invalid-email",
 63				Age:   30,
 64			},
 65			wantErr: true,
 66			errMsg:  "invalid email format",
 67		},
 68		{
 69			name: "negative age",
 70			user: User{
 71				Name:  "John Doe",
 72				Email: "john@example.com",
 73				Age:   -5,
 74			},
 75			wantErr: true,
 76			errMsg:  "invalid age",
 77		},
 78		{
 79			name: "age too high",
 80			user: User{
 81				Name:  "John Doe",
 82				Email: "john@example.com",
 83				Age:   200,
 84			},
 85			wantErr: true,
 86			errMsg:  "invalid age",
 87		},
 88	}
 89
 90	for _, tt := range tests {
 91		t.Run(tt.name, func(t *testing.T) {
 92			err := ValidateUser(tt.user)
 93
 94			if tt.wantErr {
 95				if err == nil {
 96					t.Errorf("Expected error but got nil")
 97					return
 98				}
 99				if err.Error() != tt.errMsg {
100					t.Errorf("Expected error %q, got %q", tt.errMsg, err.Error())
101				}
102			} else {
103				if err != nil {
104					t.Errorf("Unexpected error: %v", err)
105				}
106			}
107		})
108	}
109}
110
111func main() {
112	fmt.Println("Table-Driven Testing Patterns")
113	fmt.Println("=============================\n")
114
115	fmt.Println("Benefits of Table-Driven Tests:")
116	fmt.Println("- Easy to add new test cases")
117	fmt.Println("- Reduces code duplication")
118	fmt.Println("- Makes test patterns consistent")
119	fmt.Println("- Simplifies maintenance")
120	fmt.Println("- Encourages thorough testing")
121
122	fmt.Println("\nTable Structure:")
123	fmt.Println("- name: Descriptive test case name")
124	fmt.Println("- input: Test input data")
125	fmt.Println("- want: Expected output")
126	fmt.Println("- wantErr: Whether error is expected")
127
128	fmt.Println("\nExample test cases:")
129	users := []User{
130		{Name: "John Doe", Email: "john@example.com", Age: 30},
131		{Name: "", Email: "john@example.com", Age: 30},
132		{Name: "Jane Doe", Email: "invalid", Age: 25},
133	}
134
135	for i, u := range users {
136		err := ValidateUser(u)
137		if err != nil {
138			fmt.Printf("%d. %s - Error: %v\n", i+1, u.Name, err)
139		} else {
140			fmt.Printf("%d. %s - Valid\n", i+1, u.Name)
141		}
142	}
143}

Mocking and Test Doubles

Think of movie production where stunt doubles perform dangerous scenes. The double looks like the actor but can safely perform actions that would be risky for the real actor. In testing, mocks and test doubles serve a similar purpose - they stand in for real dependencies, allowing you to test code safely and predictably.

Mocks simulate dependencies to test code in isolation.

Types of Test Doubles:

  • Mock: Simulates behavior and verifies interactions
  • Stub: Returns predefined responses
  • Fake: Working implementation with shortcuts (in-memory database)
  • Spy: Records information about calls
  • Dummy: Placeholder that's never actually used

⚠️ Important: Don't over-mock. Mock at architectural boundaries (databases, external APIs, file systems) but not internal functions. Over-mocking makes tests brittle and ties them too closely to implementation details.

Manual Mocking with Interfaces

  1package main
  2
  3import (
  4	"errors"
  5	"testing"
  6)
  7
  8// Real implementation
  9type Database interface {
 10	GetUser(id string) (*User, error)
 11	SaveUser(user *User) error
 12}
 13
 14type User struct {
 15	ID    string
 16	Name  string
 17	Email string
 18}
 19
 20type PostgresDB struct {
 21	// Connection details
 22}
 23
 24func (db *PostgresDB) GetUser(id string) (*User, error) {
 25	// Real database query
 26	return nil, nil
 27}
 28
 29func (db *PostgresDB) SaveUser(user *User) error {
 30	// Real database write
 31	return nil
 32}
 33
 34// Service that depends on database
 35type UserService struct {
 36	db Database
 37}
 38
 39func NewUserService(db Database) *UserService {
 40	return &UserService{db: db}
 41}
 42
 43func (s *UserService) GetUserProfile(id string) (*User, error) {
 44	user, err := s.db.GetUser(id)
 45	if err != nil {
 46		return nil, err
 47	}
 48
 49	// Add some business logic
 50	if user.Email == "" {
 51		user.Email = "no-email@example.com"
 52	}
 53
 54	return user, nil
 55}
 56
 57// Mock implementation for testing
 58type MockDatabase struct {
 59	GetUserFunc  func(id string) (*User, error)
 60	SaveUserFunc func(user *User) error
 61	GetUserCalls int
 62}
 63
 64func (m *MockDatabase) GetUser(id string) (*User, error) {
 65	m.GetUserCalls++
 66	if m.GetUserFunc != nil {
 67		return m.GetUserFunc(id)
 68	}
 69	return nil, errors.New("not implemented")
 70}
 71
 72func (m *MockDatabase) SaveUser(user *User) error {
 73	if m.SaveUserFunc != nil {
 74		return m.SaveUserFunc(user)
 75	}
 76	return errors.New("not implemented")
 77}
 78
 79// Tests using the mock
 80func TestUserService_GetUserProfile(t *testing.T) {
 81	t.Run("user exists", func(t *testing.T) {
 82		mockDB := &MockDatabase{
 83			GetUserFunc: func(id string) (*User, error) {
 84				return &User{
 85					ID:    id,
 86					Name:  "John Doe",
 87					Email: "john@example.com",
 88				}, nil
 89			},
 90		}
 91
 92		service := NewUserService(mockDB)
 93		user, err := service.GetUserProfile("123")
 94
 95		if err != nil {
 96			t.Fatalf("Unexpected error: %v", err)
 97		}
 98
 99		if user.Name != "John Doe" {
100			t.Errorf("Expected name 'John Doe', got %s", user.Name)
101		}
102
103		if mockDB.GetUserCalls != 1 {
104			t.Errorf("Expected 1 GetUser call, got %d", mockDB.GetUserCalls)
105		}
106	})
107
108	t.Run("user not found", func(t *testing.T) {
109		mockDB := &MockDatabase{
110			GetUserFunc: func(id string) (*User, error) {
111				return nil, errors.New("user not found")
112			},
113		}
114
115		service := NewUserService(mockDB)
116		_, err := service.GetUserProfile("999")
117
118		if err == nil {
119			t.Error("Expected error, got nil")
120		}
121	})
122
123	t.Run("adds default email", func(t *testing.T) {
124		mockDB := &MockDatabase{
125			GetUserFunc: func(id string) (*User, error) {
126				return &User{
127					ID:    id,
128					Name:  "Jane Doe",
129					Email: "", // Empty email
130				}, nil
131			},
132		}
133
134		service := NewUserService(mockDB)
135		user, err := service.GetUserProfile("456")
136
137		if err != nil {
138			t.Fatalf("Unexpected error: %v", err)
139		}
140
141		if user.Email != "no-email@example.com" {
142			t.Errorf("Expected default email, got %s", user.Email)
143		}
144	})
145}

Advanced Mocking Patterns

  1// run
  2package main
  3
  4import (
  5	"errors"
  6	"fmt"
  7	"time"
  8)
  9
 10// HTTP Client interface
 11type HTTPClient interface {
 12	Get(url string) (*Response, error)
 13	Post(url string, body []byte) (*Response, error)
 14}
 15
 16type Response struct {
 17	StatusCode int
 18	Body       []byte
 19}
 20
 21// Service using HTTP client
 22type APIClient struct {
 23	client  HTTPClient
 24	baseURL string
 25	timeout time.Duration
 26}
 27
 28func NewAPIClient(client HTTPClient, baseURL string) *APIClient {
 29	return &APIClient{
 30		client:  client,
 31		baseURL: baseURL,
 32		timeout: 30 * time.Second,
 33	}
 34}
 35
 36func (c *APIClient) FetchData(endpoint string) ([]byte, error) {
 37	url := c.baseURL + endpoint
 38
 39	resp, err := c.client.Get(url)
 40	if err != nil {
 41		return nil, fmt.Errorf("request failed: %w", err)
 42	}
 43
 44	if resp.StatusCode != 200 {
 45		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
 46	}
 47
 48	return resp.Body, nil
 49}
 50
 51// Configurable mock with multiple scenarios
 52type MockHTTPClient struct {
 53	responses map[string]*Response
 54	errors    map[string]error
 55	calls     []string
 56}
 57
 58func NewMockHTTPClient() *MockHTTPClient {
 59	return &MockHTTPClient{
 60		responses: make(map[string]*Response),
 61		errors:    make(map[string]error),
 62		calls:     make([]string, 0),
 63	}
 64}
 65
 66func (m *MockHTTPClient) AddResponse(url string, statusCode int, body []byte) {
 67	m.responses[url] = &Response{
 68		StatusCode: statusCode,
 69		Body:       body,
 70	}
 71}
 72
 73func (m *MockHTTPClient) AddError(url string, err error) {
 74	m.errors[url] = err
 75}
 76
 77func (m *MockHTTPClient) Get(url string) (*Response, error) {
 78	m.calls = append(m.calls, "GET "+url)
 79
 80	if err, exists := m.errors[url]; exists {
 81		return nil, err
 82	}
 83
 84	if resp, exists := m.responses[url]; exists {
 85		return resp, nil
 86	}
 87
 88	return nil, errors.New("no mock configured for URL")
 89}
 90
 91func (m *MockHTTPClient) Post(url string, body []byte) (*Response, error) {
 92	m.calls = append(m.calls, "POST "+url)
 93	return nil, errors.New("not implemented")
 94}
 95
 96func (m *MockHTTPClient) GetCalls() []string {
 97	return m.calls
 98}
 99
100func main() {
101	fmt.Println("Advanced Mocking Patterns")
102	fmt.Println("=========================\n")
103
104	// Test successful request
105	fmt.Println("Test 1: Successful Request")
106	mockClient := NewMockHTTPClient()
107	mockClient.AddResponse("https://api.example.com/data", 200, []byte(`{"result": "success"}`))
108
109	client := NewAPIClient(mockClient, "https://api.example.com")
110	data, err := client.FetchData("/data")
111
112	if err != nil {
113		fmt.Printf("Error: %v\n", err)
114	} else {
115		fmt.Printf("Success: %s\n", string(data))
116	}
117
118	// Test error scenario
119	fmt.Println("\nTest 2: Network Error")
120	mockClient2 := NewMockHTTPClient()
121	mockClient2.AddError("https://api.example.com/error", errors.New("connection timeout"))
122
123	client2 := NewAPIClient(mockClient2, "https://api.example.com")
124	_, err = client2.FetchData("/error")
125
126	if err != nil {
127		fmt.Printf("Expected error: %v\n", err)
128	}
129
130	// Test status code handling
131	fmt.Println("\nTest 3: Non-200 Status Code")
132	mockClient3 := NewMockHTTPClient()
133	mockClient3.AddResponse("https://api.example.com/notfound", 404, []byte("Not Found"))
134
135	client3 := NewAPIClient(mockClient3, "https://api.example.com")
136	_, err = client3.FetchData("/notfound")
137
138	if err != nil {
139		fmt.Printf("Expected error: %v\n", err)
140	}
141
142	fmt.Println("\nMocking Best Practices:")
143	fmt.Println("- Define interfaces for dependencies")
144	fmt.Println("- Mock at architectural boundaries")
145	fmt.Println("- Verify interactions when necessary")
146	fmt.Println("- Use table-driven tests with mocks")
147	fmt.Println("- Keep mocks simple and focused")
148}

Testing HTTP Handlers

Web applications are like restaurants - customers (clients) make requests (orders), servers (handlers) process them, and responses (meals) are delivered. Testing HTTP handlers ensures your application serves the right responses to various requests.

HTTP handler tests verify request processing and response generation.

What to Test in HTTP Handlers:

  • Response status codes
  • Response headers
  • Response body content
  • Request parameter handling
  • Authentication/authorization
  • Error handling
  • Content negotiation

Basic HTTP Handler Testing

 1package main
 2
 3import (
 4	"encoding/json"
 5	"net/http"
 6	"net/http/httptest"
 7	"strings"
 8	"testing"
 9)
10
11// Handler to test
12type User struct {
13	ID   string `json:"id"`
14	Name string `json:"name"`
15}
16
17func UserHandler(w http.ResponseWriter, r *http.Request) {
18	if r.Method != http.MethodGet {
19		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
20		return
21	}
22
23	id := r.URL.Query().Get("id")
24	if id == "" {
25		http.Error(w, "Missing id parameter", http.StatusBadRequest)
26		return
27	}
28
29	user := User{
30		ID:   id,
31		Name: "John Doe",
32	}
33
34	w.Header().Set("Content-Type", "application/json")
35	json.NewEncoder(w).Encode(user)
36}
37
38func TestUserHandler(t *testing.T) {
39	tests := []struct {
40		name           string
41		method         string
42		url            string
43		wantStatusCode int
44		wantBody       string
45	}{
46		{
47			name:           "valid request",
48			method:         http.MethodGet,
49			url:            "/user?id=123",
50			wantStatusCode: http.StatusOK,
51			wantBody:       `{"id":"123","name":"John Doe"}`,
52		},
53		{
54			name:           "missing id",
55			method:         http.MethodGet,
56			url:            "/user",
57			wantStatusCode: http.StatusBadRequest,
58			wantBody:       "Missing id parameter",
59		},
60		{
61			name:           "wrong method",
62			method:         http.MethodPost,
63			url:            "/user?id=123",
64			wantStatusCode: http.StatusMethodNotAllowed,
65			wantBody:       "Method not allowed",
66		},
67	}
68
69	for _, tt := range tests {
70		t.Run(tt.name, func(t *testing.T) {
71			req := httptest.NewRequest(tt.method, tt.url, nil)
72			w := httptest.NewRecorder()
73
74			UserHandler(w, req)
75
76			if w.Code != tt.wantStatusCode {
77				t.Errorf("Status code = %d, want %d", w.Code, tt.wantStatusCode)
78			}
79
80			body := strings.TrimSpace(w.Body.String())
81			if !strings.Contains(body, strings.TrimSpace(tt.wantBody)) {
82				t.Errorf("Body = %q, want %q", body, tt.wantBody)
83			}
84		})
85	}
86}

Testing with Middleware and Context

 1// run
 2package main
 3
 4import (
 5	"context"
 6	"fmt"
 7	"net/http"
 8	"net/http/httptest"
 9	"testing"
10)
11
12type contextKey string
13
14const userIDKey contextKey = "userID"
15
16// Middleware
17func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
18	return func(w http.ResponseWriter, r *http.Request) {
19		token := r.Header.Get("Authorization")
20
21		if token == "" {
22			http.Error(w, "Unauthorized", http.StatusUnauthorized)
23			return
24		}
25
26		// Simulate token validation
27		userID := "user-123" // Extracted from token
28		ctx := context.WithValue(r.Context(), userIDKey, userID)
29
30		next(w, r.WithContext(ctx))
31	}
32}
33
34// Handler that requires authentication
35func ProfileHandler(w http.ResponseWriter, r *http.Request) {
36	userID, ok := r.Context().Value(userIDKey).(string)
37	if !ok {
38		http.Error(w, "No user in context", http.StatusInternalServerError)
39		return
40	}
41
42	fmt.Fprintf(w, "Profile for user: %s", userID)
43}
44
45func TestProfileHandler_WithAuth(t *testing.T) {
46	t.Run("with valid token", func(t *testing.T) {
47		req := httptest.NewRequest(http.MethodGet, "/profile", nil)
48		req.Header.Set("Authorization", "Bearer valid-token")
49		w := httptest.NewRecorder()
50
51		// Test with middleware
52		handler := AuthMiddleware(ProfileHandler)
53		handler(w, req)
54
55		if w.Code != http.StatusOK {
56			t.Errorf("Status code = %d, want %d", w.Code, http.StatusOK)
57		}
58
59		expected := "Profile for user: user-123"
60		if w.Body.String() != expected {
61			t.Errorf("Body = %q, want %q", w.Body.String(), expected)
62		}
63	})
64
65	t.Run("without token", func(t *testing.T) {
66		req := httptest.NewRequest(http.MethodGet, "/profile", nil)
67		w := httptest.NewRecorder()
68
69		handler := AuthMiddleware(ProfileHandler)
70		handler(w, req)
71
72		if w.Code != http.StatusUnauthorized {
73			t.Errorf("Status code = %d, want %d", w.Code, http.StatusUnauthorized)
74		}
75	})
76}
77
78func main() {
79	fmt.Println("Testing HTTP Handlers with Middleware")
80	fmt.Println("=====================================\n")
81
82	fmt.Println("Test Components:")
83	fmt.Println("- httptest.NewRequest(): Create test requests")
84	fmt.Println("- httptest.NewRecorder(): Record responses")
85	fmt.Println("- Context values: Test data flow through middleware")
86	fmt.Println("- Headers: Test authentication and content negotiation")
87
88	fmt.Println("\nCommon Test Scenarios:")
89	fmt.Println("- Valid requests with expected responses")
90	fmt.Println("- Missing or invalid parameters")
91	fmt.Println("- Authentication and authorization")
92	fmt.Println("- Error handling and status codes")
93	fmt.Println("- Content type negotiation")
94	fmt.Println("- Rate limiting and throttling")
95}

Benchmarking

Imagine two runners training for a marathon. Simply finishing the race isn't enough - they need to know their time, pace, and how different training methods affect performance. Benchmarks serve the same purpose for code, measuring performance and helping you optimize effectively.

Benchmarks measure code performance and identify optimization opportunities.

When to Benchmark:

  • Performance-critical code paths
  • Before and after optimizations
  • Comparing algorithm implementations
  • Detecting performance regressions
  • Capacity planning

⚠️ Important: "Premature optimization is the root of all evil" - Donald Knuth. Always benchmark to verify performance problems exist before optimizing. Profile first, then optimize hot paths.

Writing Benchmarks

 1package main
 2
 3import (
 4	"fmt"
 5	"strings"
 6	"testing"
 7)
 8
 9// Functions to benchmark
10func ConcatWithPlus(strs []string) string {
11	result := ""
12	for _, s := range strs {
13		result += s
14	}
15	return result
16}
17
18func ConcatWithBuilder(strs []string) string {
19	var builder strings.Builder
20	for _, s := range strs {
21		builder.WriteString(s)
22	}
23	return builder.String()
24}
25
26func ConcatWithJoin(strs []string) string {
27	return strings.Join(strs, "")
28}
29
30// Benchmarks
31func BenchmarkConcatWithPlus(b *testing.B) {
32	strs := []string{"hello", " ", "world", "!"}
33
34	b.ResetTimer()
35	for i := 0; i < b.N; i++ {
36		ConcatWithPlus(strs)
37	}
38}
39
40func BenchmarkConcatWithBuilder(b *testing.B) {
41	strs := []string{"hello", " ", "world", "!"}
42
43	b.ResetTimer()
44	for i := 0; i < b.N; i++ {
45		ConcatWithBuilder(strs)
46	}
47}
48
49func BenchmarkConcatWithJoin(b *testing.B) {
50	strs := []string{"hello", " ", "world", "!"}
51
52	b.ResetTimer()
53	for i := 0; i < b.N; i++ {
54		ConcatWithJoin(strs)
55	}
56}
57
58// Benchmark with different input sizes
59func BenchmarkConcatScaling(b *testing.B) {
60	sizes := []int{10, 100, 1000}
61
62	for _, size := range sizes {
63		strs := make([]string, size)
64		for i := range strs {
65			strs[i] = "x"
66		}
67
68		b.Run(fmt.Sprintf("Plus_%d", size), func(b *testing.B) {
69			for i := 0; i < b.N; i++ {
70				ConcatWithPlus(strs)
71			}
72		})
73
74		b.Run(fmt.Sprintf("Builder_%d", size), func(b *testing.B) {
75			for i := 0; i < b.N; i++ {
76				ConcatWithBuilder(strs)
77			}
78		})
79	}
80}

Memory Benchmarks

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"testing"
 7)
 8
 9// Memory-intensive operations
10func AllocateSlice(size int) []int {
11	return make([]int, size)
12}
13
14func AllocateSliceWithCapacity(size int) []int {
15	slice := make([]int, 0, size)
16	for i := 0; i < size; i++ {
17		slice = append(slice, i)
18	}
19	return slice
20}
21
22func BenchmarkMemoryAllocation(b *testing.B) {
23	b.Run("WithoutCapacity", func(b *testing.B) {
24		b.ReportAllocs() // Report allocation statistics
25		for i := 0; i < b.N; i++ {
26			slice := make([]int, 0)
27			for j := 0; j < 1000; j++ {
28				slice = append(slice, j)
29			}
30		}
31	})
32
33	b.Run("WithCapacity", func(b *testing.B) {
34		b.ReportAllocs()
35		for i := 0; i < b.N; i++ {
36			slice := make([]int, 0, 1000)
37			for j := 0; j < 1000; j++ {
38				slice = append(slice, j)
39			}
40		}
41	})
42}
43
44func main() {
45	fmt.Println("Benchmark Analysis")
46	fmt.Println("==================\n")
47
48	fmt.Println("Running Benchmarks:")
49	fmt.Println("  go test -bench=. -benchmem")
50
51	fmt.Println("\nBenchmark Output Format:")
52	fmt.Println("  BenchmarkName-8    1000000    1234 ns/op    128 B/op    3 allocs/op")
53	fmt.Println("                      ^          ^            ^           ^")
54	fmt.Println("                      |          |            |           |")
55	fmt.Println("                  iterations  time/op   bytes/op    allocations")
56
57	fmt.Println("\nBenchmark Best Practices:")
58	fmt.Println("- Use b.ResetTimer() to exclude setup time")
59	fmt.Println("- Use b.ReportAllocs() to track memory")
60	fmt.Println("- Run benchmarks multiple times for consistency")
61	fmt.Println("- Benchmark on target hardware")
62	fmt.Println("- Compare before and after optimizations")
63
64	fmt.Println("\nPerformance Tips:")
65	fmt.Println("- Pre-allocate slices when size is known")
66	fmt.Println("- Use sync.Pool for frequently allocated objects")
67	fmt.Println("- Avoid unnecessary allocations in hot paths")
68	fmt.Println("- Profile before optimizing")
69}

Integration Testing

While unit tests verify individual components, integration tests ensure those components work together correctly. Think of building with LEGO blocks - you test each block individually, but you also need to verify they snap together properly to build the complete structure.

Integration tests verify components work correctly together.

Integration Test Scope:

  • Database interactions
  • External API calls
  • File system operations
  • Message queue communication
  • Service-to-service communication

Database Integration Tests

 1package main
 2
 3import (
 4	"database/sql"
 5	"testing"
 6
 7	_ "github.com/lib/pq"
 8)
 9
10type UserRepository struct {
11	db *sql.DB
12}
13
14func NewUserRepository(db *sql.DB) *UserRepository {
15	return &UserRepository{db: db}
16}
17
18func (r *UserRepository) Create(user *User) error {
19	query := `INSERT INTO users (id, name, email) VALUES ($1, $2, $3)`
20	_, err := r.db.Exec(query, user.ID, user.Name, user.Email)
21	return err
22}
23
24func (r *UserRepository) GetByID(id string) (*User, error) {
25	query := `SELECT id, name, email FROM users WHERE id = $1`
26
27	user := &User{}
28	err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email)
29	if err != nil {
30		return nil, err
31	}
32
33	return user, nil
34}
35
36// Integration test
37func TestUserRepository_Integration(t *testing.T) {
38	if testing.Short() {
39		t.Skip("Skipping integration test")
40	}
41
42	// Setup test database
43	db, err := sql.Open("postgres", "postgres://localhost/testdb?sslmode=disable")
44	if err != nil {
45		t.Fatalf("Failed to connect to database: %v", err)
46	}
47	defer db.Close()
48
49	// Clean up before test
50	db.Exec("DELETE FROM users")
51
52	repo := NewUserRepository(db)
53
54	t.Run("create and retrieve user", func(t *testing.T) {
55		user := &User{
56			ID:    "test-123",
57			Name:  "Test User",
58			Email: "test@example.com",
59		}
60
61		// Create user
62		err := repo.Create(user)
63		if err != nil {
64			t.Fatalf("Failed to create user: %v", err)
65		}
66
67		// Retrieve user
68		retrieved, err := repo.GetByID(user.ID)
69		if err != nil {
70			t.Fatalf("Failed to retrieve user: %v", err)
71		}
72
73		// Verify
74		if retrieved.Name != user.Name {
75			t.Errorf("Name = %s, want %s", retrieved.Name, user.Name)
76		}
77		if retrieved.Email != user.Email {
78			t.Errorf("Email = %s, want %s", retrieved.Email, user.Email)
79		}
80	})
81
82	// Cleanup after test
83	db.Exec("DELETE FROM users WHERE id = $1", "test-123")
84}

Testing with Docker Test Containers

 1// run
 2package main
 3
 4import (
 5	"context"
 6	"fmt"
 7	"time"
 8)
 9
10// Simulated test container management
11type TestContainer struct {
12	ID      string
13	Name    string
14	Port    int
15	Running bool
16}
17
18func StartPostgresContainer(ctx context.Context) (*TestContainer, error) {
19	// In real implementation, would use testcontainers-go library
20	container := &TestContainer{
21		ID:      "postgres-test-123",
22		Name:    "postgres",
23		Port:    5432,
24		Running: true,
25	}
26
27	fmt.Println("Starting PostgreSQL container...")
28	time.Sleep(100 * time.Millisecond) // Simulate startup
29	fmt.Println("Container ready at port", container.Port)
30
31	return container, nil
32}
33
34func (c *TestContainer) Stop() error {
35	fmt.Println("Stopping container", c.ID)
36	c.Running = false
37	return nil
38}
39
40func TestWithContainer(t *testing.T) {
41	if testing.Short() {
42		t.Skip("Skipping container test")
43	}
44
45	ctx := context.Background()
46
47	// Start container
48	container, err := StartPostgresContainer(ctx)
49	if err != nil {
50		t.Fatalf("Failed to start container: %v", err)
51	}
52	defer container.Stop()
53
54	// Run tests against container
55	// ...
56}
57
58func main() {
59	fmt.Println("Integration Testing with Containers")
60	fmt.Println("===================================\n")
61
62	fmt.Println("Benefits of Test Containers:")
63	fmt.Println("- Isolated test environment")
64	fmt.Println("- Consistent across developers")
65	fmt.Println("- No shared test database conflicts")
66	fmt.Println("- Test against real dependencies")
67	fmt.Println("- Automatic cleanup")
68
69	fmt.Println("\nTest Container Workflow:")
70	fmt.Println("1. Start container before tests")
71	fmt.Println("2. Wait for container to be ready")
72	fmt.Println("3. Run integration tests")
73	fmt.Println("4. Stop and remove container")
74
75	// Demonstrate container lifecycle
76	ctx := context.Background()
77	container, _ := StartPostgresContainer(ctx)
78	fmt.Printf("\nContainer %s running on port %d\n", container.Name, container.Port)
79	container.Stop()
80
81	fmt.Println("\nPopular Test Container Libraries:")
82	fmt.Println("- testcontainers-go: Docker containers for Go tests")
83	fmt.Println("- dockertest: Lightweight Docker integration")
84	fmt.Println("- ory/dockertest: Database testing with Docker")
85}

Test Coverage

Test coverage tells you which parts of your code are executed by tests, but it doesn't tell you if those tests are good. Think of coverage like a map showing where you've explored - it shows what you've seen, but not whether you understood what you saw.

Test coverage measures code execution during tests, not test quality.

Coverage Metrics:

  • Line Coverage: Percentage of lines executed
  • Branch Coverage: Percentage of decision branches taken
  • Function Coverage: Percentage of functions called

πŸ’‘ Key Takeaway: Aim for 70-80% coverage as a guideline, not a target. 100% coverage doesn't guarantee bug-free code. Focus on testing critical paths and edge cases, not just hitting coverage numbers.

Measuring Coverage

 1// run
 2package main
 3
 4import (
 5	"errors"
 6	"fmt"
 7)
 8
 9// Function with multiple branches
10func ProcessOrder(amount float64, vipCustomer bool) (float64, error) {
11	if amount < 0 {
12		return 0, errors.New("negative amount")
13	}
14
15	if amount == 0 {
16		return 0, errors.New("zero amount")
17	}
18
19	discount := 0.0
20	if vipCustomer {
21		discount = 0.10 // 10% discount for VIP
22	}
23
24	total := amount * (1 - discount)
25
26	if total > 1000 {
27		// Free shipping for orders over $1000
28		total -= 10
29	}
30
31	return total, nil
32}
33
34func main() {
35	fmt.Println("Test Coverage Analysis")
36	fmt.Println("=====================\n")
37
38	fmt.Println("Running Coverage:")
39	fmt.Println("  go test -cover")
40	fmt.Println("  go test -coverprofile=coverage.out")
41	fmt.Println("  go tool cover -html=coverage.out")
42
43	fmt.Println("\nCoverage Report:")
44	fmt.Println("  PASS")
45	fmt.Println("  coverage: 85.7% of statements")
46
47	fmt.Println("\nBranches to Test:")
48	testCases := []struct {
49		amount      float64
50		vipCustomer bool
51		description string
52	}{
53		{-10, false, "negative amount"},
54		{0, false, "zero amount"},
55		{100, false, "normal order"},
56		{100, true, "VIP discount"},
57		{1500, false, "free shipping threshold"},
58		{1500, true, "VIP + free shipping"},
59	}
60
61	for i, tc := range testCases {
62		result, err := ProcessOrder(tc.amount, tc.vipCustomer)
63		status := "βœ“"
64		if err != nil {
65			status = "βœ—"
66		}
67		fmt.Printf("%d. %s %s: %.2f\n", i+1, status, tc.description, result)
68	}
69
70	fmt.Println("\nCoverage Guidelines:")
71	fmt.Println("- 70-80%: Good coverage for most projects")
72	fmt.Println("- 80-90%: High coverage, diminishing returns")
73	fmt.Println("- 90-100%: Excessive, may indicate over-testing")
74	fmt.Println("\nWhat Coverage Doesn't Tell You:")
75	fmt.Println("- Test quality and assertions")
76	fmt.Println("- Edge cases and boundary conditions")
77	fmt.Println("- Integration issues")
78	fmt.Println("- Real-world usage patterns")
79}

Testing Best Practices

Think of testing like building a house. You need a solid foundation (test infrastructure), strong walls (test organization), and a good roof (continuous integration). Skip any of these, and the whole structure becomes unstable.

Test Organization

  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"os"
  7	"testing"
  8)
  9
 10// Test helpers with t.Helper()
 11func assertNoError(t *testing.T, err error) {
 12	t.Helper()
 13	if err != nil {
 14		t.Fatalf("unexpected error: %v", err)
 15	}
 16}
 17
 18func assertEqual(t *testing.T, got, want interface{}) {
 19	t.Helper()
 20	if got != want {
 21		t.Errorf("got %v, want %v", got, want)
 22	}
 23}
 24
 25// Using TestMain for setup and teardown
 26func TestMain(m *testing.M) {
 27	// Setup code runs before all tests
 28	setup()
 29
 30	// Run all tests
 31	code := m.Run()
 32
 33	// Teardown code runs after all tests
 34	teardown()
 35
 36	// Exit with test result code
 37	os.Exit(code)
 38}
 39
 40func setup() {
 41	// Initialize test database, create temp files, etc.
 42	fmt.Println("Setting up test environment...")
 43}
 44
 45func teardown() {
 46	// Clean up resources
 47	fmt.Println("Cleaning up test environment...")
 48}
 49
 50// Using t.Cleanup for automatic cleanup
 51func TestWithCleanup(t *testing.T) {
 52	// Create a resource
 53	resource := "test-resource"
 54
 55	// Register cleanup - will run even if test fails
 56	t.Cleanup(func() {
 57		// Clean up resource
 58		fmt.Println("Cleaning up", resource)
 59	})
 60
 61	// Test code...
 62}
 63
 64// Parallel tests for better performance
 65func TestParallel1(t *testing.T) {
 66	t.Parallel() // Run in parallel with other parallel tests
 67
 68	// Test code...
 69}
 70
 71func TestParallel2(t *testing.T) {
 72	t.Parallel()
 73
 74	// Test code...
 75}
 76
 77// Skipping tests conditionally
 78func TestRequiresDocker(t *testing.T) {
 79	if os.Getenv("DOCKER_HOST") == "" {
 80		t.Skip("Docker not available")
 81	}
 82
 83	// Test code that requires Docker...
 84}
 85
 86func TestLongRunning(t *testing.T) {
 87	if testing.Short() {
 88		t.Skip("Skipping long-running test in short mode")
 89	}
 90
 91	// Long-running test code...
 92}
 93
 94func main() {
 95	fmt.Println("Test Organization Best Practices")
 96	fmt.Println("================================\n")
 97
 98	fmt.Println("1. File Structure:")
 99	fmt.Println("   calculator.go")
100	fmt.Println("   calculator_test.go       # Unit tests")
101	fmt.Println("   calculator_integration_test.go  # Integration tests")
102
103	fmt.Println("\n2. Test Naming:")
104	fmt.Println("   TestFunction_Scenario_ExpectedBehavior")
105	fmt.Println("   TestAdd_PositiveNumbers_ReturnsSum")
106	fmt.Println("   TestDivide_ByZero_ReturnsError")
107
108	fmt.Println("\n3. Test Tags:")
109	fmt.Println("   // +build integration")
110	fmt.Println("   Run with: go test -tags=integration")
111
112	fmt.Println("\n4. Test Helpers:")
113	fmt.Println("   - Use t.Helper() in helper functions")
114	fmt.Println("   - Create test fixtures in testdata/")
115	fmt.Println("   - Share setup code in TestMain")
116
117	fmt.Println("\n5. Parallel Testing:")
118	fmt.Println("   t.Parallel() for independent tests")
119	fmt.Println("   Speeds up test execution")
120	fmt.Println("   Be careful with shared state")
121
122	fmt.Println("\n6. Test Cleanup:")
123	fmt.Println("   t.Cleanup() for automatic cleanup")
124	fmt.Println("   defer for cleanup in order")
125	fmt.Println("   Always clean up resources")
126
127	fmt.Println("\n7. Skip Tests:")
128	fmt.Println("   t.Skip() for conditional tests")
129	fmt.Println("   testing.Short() for quick test runs")
130	fmt.Println("   Document why tests are skipped")
131
132	fmt.Println("\n8. Test Data:")
133	fmt.Println("   - Store test fixtures in testdata/ directory")
134	fmt.Println("   - Use golden files for expected outputs")
135	fmt.Println("   - Keep test data close to tests")
136}

Test Fixtures and Golden Files

Test fixtures provide consistent test data, while golden files store expected outputs for comparison testing. Think of golden files like answer keys in a textbook - they provide the "correct" output to verify your code produces the right results.

  1// run
  2package main
  3
  4import (
  5	"bytes"
  6	"encoding/json"
  7	"fmt"
  8	"io"
  9	"os"
 10	"path/filepath"
 11	"strings"
 12	"testing"
 13)
 14
 15// Sample data structure
 16type Report struct {
 17	Title   string   `json:"title"`
 18	Summary string   `json:"summary"`
 19	Items   []string `json:"items"`
 20	Total   int      `json:"total"`
 21}
 22
 23// Function that generates reports
 24func GenerateReport(items []string) (*Report, error) {
 25	if len(items) == 0 {
 26		return nil, fmt.Errorf("no items provided")
 27	}
 28
 29	report := &Report{
 30		Title:   "Monthly Report",
 31		Summary: fmt.Sprintf("Report containing %d items", len(items)),
 32		Items:   items,
 33		Total:   len(items),
 34	}
 35
 36	return report, nil
 37}
 38
 39// Function to format report as JSON
 40func FormatReportJSON(report *Report) ([]byte, error) {
 41	var buf bytes.Buffer
 42	encoder := json.NewEncoder(&buf)
 43	encoder.SetIndent("", "  ")
 44	if err := encoder.Encode(report); err != nil {
 45		return nil, err
 46	}
 47	return buf.Bytes(), nil
 48}
 49
 50// Helper: Load golden file
 51func loadGoldenFile(t *testing.T, name string) []byte {
 52	t.Helper()
 53
 54	path := filepath.Join("testdata", name+".golden")
 55	data, err := os.ReadFile(path)
 56	if err != nil {
 57		t.Fatalf("Failed to read golden file %s: %v", path, err)
 58	}
 59
 60	return data
 61}
 62
 63// Helper: Update golden file (use with -update flag)
 64func updateGoldenFile(t *testing.T, name string, data []byte) {
 65	t.Helper()
 66
 67	path := filepath.Join("testdata", name+".golden")
 68	if err := os.MkdirAll("testdata", 0755); err != nil {
 69		t.Fatalf("Failed to create testdata directory: %v", err)
 70	}
 71
 72	if err := os.WriteFile(path, data, 0644); err != nil {
 73		t.Fatalf("Failed to write golden file %s: %v", path, err)
 74	}
 75}
 76
 77// Test with golden file
 78func TestGenerateReport_GoldenFile(t *testing.T) {
 79	items := []string{"Item 1", "Item 2", "Item 3"}
 80
 81	report, err := GenerateReport(items)
 82	if err != nil {
 83		t.Fatalf("GenerateReport failed: %v", err)
 84	}
 85
 86	got, err := FormatReportJSON(report)
 87	if err != nil {
 88		t.Fatalf("FormatReportJSON failed: %v", err)
 89	}
 90
 91	// Simulate golden file comparison
 92	goldenJSON := `{
 93  "title": "Monthly Report",
 94  "summary": "Report containing 3 items",
 95  "items": [
 96    "Item 1",
 97    "Item 2",
 98    "Item 3"
 99  ],
100  "total": 3
101}
102`
103
104	gotStr := strings.TrimSpace(string(got))
105	wantStr := strings.TrimSpace(goldenJSON)
106
107	if gotStr != wantStr {
108		t.Errorf("Output doesn't match golden file")
109		t.Logf("Got:\n%s", gotStr)
110		t.Logf("Want:\n%s", wantStr)
111	}
112}
113
114// Test with fixture file
115func TestLoadFixture(t *testing.T) {
116	// Simulate loading fixture
117	fixtureData := `{
118  "title": "Test Report",
119  "summary": "Test data",
120  "items": ["A", "B"],
121  "total": 2
122}`
123
124	var report Report
125	if err := json.Unmarshal([]byte(fixtureData), &report); err != nil {
126		t.Fatalf("Failed to unmarshal fixture: %v", err)
127	}
128
129	if report.Title != "Test Report" {
130		t.Errorf("Title = %s, want Test Report", report.Title)
131	}
132
133	if report.Total != 2 {
134		t.Errorf("Total = %d, want 2", report.Total)
135	}
136}
137
138func main() {
139	fmt.Println("Test Fixtures and Golden Files")
140	fmt.Println("==============================\n")
141
142	fmt.Println("Test Fixtures:")
143	fmt.Println("- Store test data in testdata/ directory")
144	fmt.Println("- Go's testing package ignores testdata/ by convention")
145	fmt.Println("- Use fixtures for complex input data")
146	fmt.Println("- Keep fixtures close to tests that use them")
147
148	fmt.Println("\nGolden Files:")
149	fmt.Println("- Store expected output in .golden files")
150	fmt.Println("- Compare actual output against golden file")
151	fmt.Println("- Use -update flag to update golden files")
152	fmt.Println("- Review golden file changes in version control")
153
154	fmt.Println("\nDirectory Structure:")
155	fmt.Println("  mypackage/")
156	fmt.Println("    mypackage.go")
157	fmt.Println("    mypackage_test.go")
158	fmt.Println("    testdata/")
159	fmt.Println("      input.json       # Test fixture")
160	fmt.Println("      output.golden    # Expected output")
161
162	fmt.Println("\nBest Practices:")
163	fmt.Println("- Use descriptive fixture names")
164	fmt.Println("- Keep fixtures minimal and focused")
165	fmt.Println("- Version control fixtures and golden files")
166	fmt.Println("- Document fixture format and purpose")
167
168	// Demonstrate report generation
169	items := []string{"Item 1", "Item 2", "Item 3"}
170	report, err := GenerateReport(items)
171	if err != nil {
172		fmt.Printf("Error: %v\n", err)
173		return
174	}
175
176	output, err := FormatReportJSON(report)
177	if err != nil {
178		fmt.Printf("Error: %v\n", err)
179		return
180	}
181
182	fmt.Println("\nGenerated Report:")
183	fmt.Println(string(output))
184}

Testing Common Patterns

Real-world applications require testing various patterns beyond basic unit tests. Let's explore testing common patterns like retries, timeouts, and concurrent operations.

Testing Retry Logic

  1// run
  2package main
  3
  4import (
  5	"errors"
  6	"fmt"
  7	"time"
  8)
  9
 10// RetryableOperation simulates an operation that might fail
 11type RetryableOperation struct {
 12	attempts     int
 13	maxAttempts  int
 14	failUntil    int
 15	callCount    int
 16}
 17
 18func NewRetryableOperation(maxAttempts, failUntil int) *RetryableOperation {
 19	return &RetryableOperation{
 20		maxAttempts: maxAttempts,
 21		failUntil:   failUntil,
 22	}
 23}
 24
 25func (r *RetryableOperation) Execute() error {
 26	r.callCount++
 27
 28	if r.callCount <= r.failUntil {
 29		return errors.New("temporary failure")
 30	}
 31
 32	return nil
 33}
 34
 35func (r *RetryableOperation) GetCallCount() int {
 36	return r.callCount
 37}
 38
 39// Retry with exponential backoff
 40func RetryWithBackoff(operation func() error, maxAttempts int) error {
 41	var lastErr error
 42
 43	for attempt := 1; attempt <= maxAttempts; attempt++ {
 44		err := operation()
 45		if err == nil {
 46			return nil
 47		}
 48
 49		lastErr = err
 50
 51		if attempt < maxAttempts {
 52			// Exponential backoff: 100ms, 200ms, 400ms, 800ms...
 53			backoff := time.Duration(100*(1<<uint(attempt-1))) * time.Millisecond
 54			time.Sleep(backoff)
 55		}
 56	}
 57
 58	return fmt.Errorf("max retries reached: %w", lastErr)
 59}
 60
 61func main() {
 62	fmt.Println("Testing Retry Logic")
 63	fmt.Println("===================\n")
 64
 65	// Test 1: Operation succeeds on first try
 66	fmt.Println("Test 1: Success on first attempt")
 67	op1 := NewRetryableOperation(3, 0)
 68	err := RetryWithBackoff(op1.Execute, 3)
 69	if err == nil {
 70		fmt.Printf("βœ“ Succeeded after %d attempts\n", op1.GetCallCount())
 71	} else {
 72		fmt.Printf("βœ— Failed: %v\n", err)
 73	}
 74
 75	// Test 2: Operation succeeds after retries
 76	fmt.Println("\nTest 2: Success after 2 failures")
 77	op2 := NewRetryableOperation(3, 2)
 78	err = RetryWithBackoff(op2.Execute, 3)
 79	if err == nil {
 80		fmt.Printf("βœ“ Succeeded after %d attempts\n", op2.GetCallCount())
 81	} else {
 82		fmt.Printf("βœ— Failed: %v\n", err)
 83	}
 84
 85	// Test 3: Operation fails all attempts
 86	fmt.Println("\nTest 3: Failure after max retries")
 87	op3 := NewRetryableOperation(3, 5)
 88	err = RetryWithBackoff(op3.Execute, 3)
 89	if err != nil {
 90		fmt.Printf("βœ“ Failed as expected after %d attempts: %v\n", op3.GetCallCount(), err)
 91	} else {
 92		fmt.Println("βœ— Should have failed")
 93	}
 94
 95	fmt.Println("\nRetry Testing Patterns:")
 96	fmt.Println("- Test immediate success")
 97	fmt.Println("- Test success after N retries")
 98	fmt.Println("- Test exhausting all retries")
 99	fmt.Println("- Verify backoff timing")
100	fmt.Println("- Check error propagation")
101}

Testing Timeout Behavior

 1// run
 2package main
 3
 4import (
 5	"context"
 6	"errors"
 7	"fmt"
 8	"time"
 9)
10
11// SlowOperation simulates a potentially slow operation
12func SlowOperation(ctx context.Context, duration time.Duration) error {
13	select {
14	case <-time.After(duration):
15		return nil
16	case <-ctx.Done():
17		return ctx.Err()
18	}
19}
20
21// OperationWithTimeout wraps an operation with a timeout
22func OperationWithTimeout(timeout time.Duration, op func(context.Context) error) error {
23	ctx, cancel := context.WithTimeout(context.Background(), timeout)
24	defer cancel()
25
26	return op(ctx)
27}
28
29func main() {
30	fmt.Println("Testing Timeout Behavior")
31	fmt.Println("========================\n")
32
33	// Test 1: Operation completes before timeout
34	fmt.Println("Test 1: Operation completes in time")
35	err := OperationWithTimeout(200*time.Millisecond, func(ctx context.Context) error {
36		return SlowOperation(ctx, 50*time.Millisecond)
37	})
38	if err == nil {
39		fmt.Println("βœ“ Operation completed successfully")
40	} else {
41		fmt.Printf("βœ— Unexpected error: %v\n", err)
42	}
43
44	// Test 2: Operation times out
45	fmt.Println("\nTest 2: Operation times out")
46	err = OperationWithTimeout(50*time.Millisecond, func(ctx context.Context) error {
47		return SlowOperation(ctx, 200*time.Millisecond)
48	})
49	if errors.Is(err, context.DeadlineExceeded) {
50		fmt.Println("βœ“ Timeout occurred as expected")
51	} else {
52		fmt.Printf("βœ— Expected timeout, got: %v\n", err)
53	}
54
55	// Test 3: Context cancellation
56	fmt.Println("\nTest 3: Manual cancellation")
57	ctx, cancel := context.WithCancel(context.Background())
58
59	go func() {
60		time.Sleep(50 * time.Millisecond)
61		cancel()
62	}()
63
64	err = SlowOperation(ctx, 200*time.Millisecond)
65	if errors.Is(err, context.Canceled) {
66		fmt.Println("βœ“ Cancellation handled correctly")
67	} else {
68		fmt.Printf("βœ— Expected cancellation, got: %v\n", err)
69	}
70
71	fmt.Println("\nTimeout Testing Patterns:")
72	fmt.Println("- Test operation completes before timeout")
73	fmt.Println("- Test timeout is triggered")
74	fmt.Println("- Test context cancellation")
75	fmt.Println("- Verify cleanup happens on timeout")
76	fmt.Println("- Test timeout edge cases (0 timeout, very long timeout)")
77}

Further Reading

Practice Exercises

Exercise 1: Unit Testing a Calculator

🎯 Learning Objectives:

  • Write comprehensive unit tests for basic functions
  • Test edge cases and error conditions
  • Use table-driven tests effectively
  • Practice test organization and naming
  • Implement proper error handling tests

⏱️ Time Estimate: 45-60 minutes
πŸ“Š Difficulty: Beginner
🌍 Real-World Context: A financial application needs a reliable calculator module for processing transactions, calculating interest, and handling currency conversions.

Task: Build a calculator package with comprehensive unit tests that cover all operations, edge cases, and error scenarios.

Requirements:

  • Basic operations: Add, Subtract, Multiply, Divide
  • Advanced operations: Power, SquareRoot, Percentage
  • Error handling for division by zero, negative square roots
  • Table-driven tests for all operations
  • Edge case testing (zero values, negative numbers, large numbers)
Complete Solution
  1// run
  2package main
  3
  4import (
  5	"errors"
  6	"fmt"
  7	"math"
  8	"testing"
  9)
 10
 11// Calculator operations
 12type Calculator struct{}
 13
 14func (c Calculator) Add(a, b float64) float64 {
 15	return a + b
 16}
 17
 18func (c Calculator) Subtract(a, b float64) float64 {
 19	return a - b
 20}
 21
 22func (c Calculator) Multiply(a, b float64) float64 {
 23	return a * b
 24}
 25
 26func (c Calculator) Divide(a, b float64) (float64, error) {
 27	if b == 0 {
 28		return 0, errors.New("division by zero")
 29	}
 30	return a / b, nil
 31}
 32
 33func (c Calculator) Power(base, exponent float64) float64 {
 34	return math.Pow(base, exponent)
 35}
 36
 37func (c Calculator) SquareRoot(n float64) (float64, error) {
 38	if n < 0 {
 39		return 0, errors.New("square root of negative number")
 40	}
 41	return math.Sqrt(n), nil
 42}
 43
 44func (c Calculator) Percentage(value, percent float64) float64 {
 45	return (value * percent) / 100
 46}
 47
 48// Tests
 49func TestCalculator_Add(t *testing.T) {
 50	calc := Calculator{}
 51
 52	tests := []struct {
 53		name string
 54		a, b float64
 55		want float64
 56	}{
 57		{"positive numbers", 2, 3, 5},
 58		{"negative numbers", -2, -3, -5},
 59		{"mixed signs", -2, 3, 1},
 60		{"with zero", 5, 0, 5},
 61		{"large numbers", 1000000, 2000000, 3000000},
 62		{"decimals", 1.5, 2.5, 4.0},
 63	}
 64
 65	for _, tt := range tests {
 66		t.Run(tt.name, func(t *testing.T) {
 67			got := calc.Add(tt.a, tt.b)
 68			if got != tt.want {
 69				t.Errorf("Add(%.2f, %.2f) = %.2f, want %.2f", tt.a, tt.b, got, tt.want)
 70			}
 71		})
 72	}
 73}
 74
 75func TestCalculator_Divide(t *testing.T) {
 76	calc := Calculator{}
 77
 78	tests := []struct {
 79		name    string
 80		a, b    float64
 81		want    float64
 82		wantErr bool
 83	}{
 84		{"normal division", 10, 2, 5, false},
 85		{"division by zero", 10, 0, 0, true},
 86		{"negative result", -10, 2, -5, false},
 87		{"decimal result", 7, 2, 3.5, false},
 88		{"zero dividend", 0, 5, 0, false},
 89	}
 90
 91	for _, tt := range tests {
 92		t.Run(tt.name, func(t *testing.T) {
 93			got, err := calc.Divide(tt.a, tt.b)
 94
 95			if tt.wantErr {
 96				if err == nil {
 97					t.Error("Expected error but got nil")
 98				}
 99				return
100			}
101
102			if err != nil {
103				t.Errorf("Unexpected error: %v", err)
104				return
105			}
106
107			if got != tt.want {
108				t.Errorf("Divide(%.2f, %.2f) = %.2f, want %.2f", tt.a, tt.b, got, tt.want)
109			}
110		})
111	}
112}
113
114func TestCalculator_SquareRoot(t *testing.T) {
115	calc := Calculator{}
116
117	tests := []struct {
118		name    string
119		n       float64
120		want    float64
121		wantErr bool
122	}{
123		{"perfect square", 16, 4, false},
124		{"non-perfect square", 10, 3.162277660168379, false},
125		{"zero", 0, 0, false},
126		{"negative number", -4, 0, true},
127		{"decimal", 2.25, 1.5, false},
128	}
129
130	for _, tt := range tests {
131		t.Run(tt.name, func(t *testing.T) {
132			got, err := calc.SquareRoot(tt.n)
133
134			if tt.wantErr {
135				if err == nil {
136					t.Error("Expected error but got nil")
137				}
138				return
139			}
140
141			if err != nil {
142				t.Errorf("Unexpected error: %v", err)
143				return
144			}
145
146			if math.Abs(got-tt.want) > 0.0001 {
147				t.Errorf("SquareRoot(%.2f) = %.10f, want %.10f", tt.n, got, tt.want)
148			}
149		})
150	}
151}
152
153func main() {
154	fmt.Println("Calculator Testing Exercise")
155	fmt.Println("===========================\n")
156
157	calc := Calculator{}
158
159	// Demonstrate operations
160	fmt.Println("Basic Operations:")
161	fmt.Printf("Add(10, 5) = %.2f\n", calc.Add(10, 5))
162	fmt.Printf("Subtract(10, 5) = %.2f\n", calc.Subtract(10, 5))
163	fmt.Printf("Multiply(10, 5) = %.2f\n", calc.Multiply(10, 5))
164
165	result, err := calc.Divide(10, 5)
166	if err == nil {
167		fmt.Printf("Divide(10, 5) = %.2f\n", result)
168	}
169
170	fmt.Println("\nAdvanced Operations:")
171	fmt.Printf("Power(2, 3) = %.2f\n", calc.Power(2, 3))
172
173	sqrt, _ := calc.SquareRoot(16)
174	fmt.Printf("SquareRoot(16) = %.2f\n", sqrt)
175
176	fmt.Printf("Percentage(200, 15) = %.2f\n", calc.Percentage(200, 15))
177
178	fmt.Println("\nError Handling:")
179	_, err = calc.Divide(10, 0)
180	if err != nil {
181		fmt.Printf("Divide by zero: %v\n", err)
182	}
183
184	_, err = calc.SquareRoot(-4)
185	if err != nil {
186		fmt.Printf("Negative square root: %v\n", err)
187	}
188}

Exercise 2: Testing HTTP API with Mocks

🎯 Learning Objectives:

  • Test HTTP handlers with httptest package
  • Create and use mocks for external dependencies
  • Test request/response handling
  • Verify status codes and response bodies
  • Test error scenarios and edge cases

⏱️ Time Estimate: 60-90 minutes
πŸ“Š Difficulty: Intermediate
🌍 Real-World Context: A weather service API needs comprehensive testing to ensure reliable responses to client requests without depending on external weather data providers.

Task: Build a weather API service with comprehensive tests using mocks for external dependencies.

Requirements:

  • GET /weather endpoint with city parameter
  • Mock external weather API
  • Test successful responses
  • Test missing parameters
  • Test external API failures
  • JSON response validation
Complete Solution

See the full Weather API testing implementation in the main article content above.

Exercise 3: Benchmark String Operations

🎯 Learning Objectives:

  • Write effective benchmarks
  • Compare performance of different implementations
  • Analyze memory allocations
  • Understand benchmark output
  • Identify performance bottlenecks

⏱️ Time Estimate: 45-60 minutes
πŸ“Š Difficulty: Intermediate
🌍 Real-World Context: A logging system processes millions of string concatenations per day. Benchmarking different approaches helps identify the most efficient implementation.

Task: Benchmark different string concatenation and manipulation techniques to find the most performant approach.

Requirements:

  • Benchmark string concatenation methods
  • Measure memory allocations
  • Test with different input sizes
  • Compare builder vs concatenation
  • Analyze results and recommendations
Complete Solution

See the string benchmarking implementation in the main article content above.

Exercise 4: Integration Test with Database

🎯 Learning Objectives:

  • Write integration tests with real databases
  • Manage test data and cleanup
  • Test transaction behavior
  • Handle database errors
  • Use test containers

⏱️ Time Estimate: 90-120 minutes
πŸ“Š Difficulty: Advanced
🌍 Real-World Context: An e-commerce application needs reliable database operations for managing products, orders, and inventory with proper transaction handling.

Task: Build a product repository with comprehensive integration tests covering CRUD operations and transactions.

Requirements:

  • Create, Read, Update, Delete operations
  • Transaction support
  • Error handling tests
  • Test data cleanup
  • Connection pooling tests
Complete Solution

See the database integration testing implementation in the main article content above.

Exercise 5: Test Coverage Analysis

🎯 Learning Objectives:

  • Measure and analyze test coverage
  • Identify untested code paths
  • Write tests for edge cases
  • Understand coverage metrics
  • Improve test quality, not just coverage

⏱️ Time Estimate: 60-75 minutes
πŸ“Š Difficulty: Intermediate
🌍 Real-World Context: A security-critical authentication system needs comprehensive test coverage to ensure all code paths are verified and edge cases are handled.

Task: Analyze test coverage for a password validation module and add tests to achieve comprehensive coverage while focusing on quality.

Requirements:

  • Generate coverage reports
  • Identify untested branches
  • Add tests for edge cases
  • Achieve 80%+ coverage
  • Document why 100% isn't always necessary
Complete Solution

See the test coverage analysis implementation in the main article content above.

Summary

Key Takeaways

  1. Testing Philosophy

    • Tests provide confidence to change code
    • Good tests are fast, focused, and clear
    • Test behavior, not implementation
    • Coverage is a guide, not a goal
  2. Unit Testing

    • Test individual functions in isolation
    • Use Arrange-Act-Assert pattern
    • Write descriptive test names
    • Test both success and failure cases
  3. Table-Driven Tests

    • Reduce code duplication
    • Easy to add new test cases
    • Consistent test structure
    • Encourage thorough testing
  4. Mocking

    • Mock at architectural boundaries
    • Use interfaces for dependency injection
    • Don't over-mock
    • Verify interactions when needed
  5. Benchmarking

    • Measure before optimizing
    • Compare different approaches
    • Track memory allocations
    • Benchmark on target hardware
  6. Integration Testing

    • Test component interactions
    • Use test containers when possible
    • Clean up test data
    • Skip in short test runs

Next Steps:

  • Learn advanced testing techniques (fuzzing, property-based testing)
  • Study test-driven development (TDD) methodology
  • Explore continuous integration and testing automation
  • Practice writing maintainable test suites

You've mastered testing fundamentals in Go - you're now ready to build reliable, well-tested applications with confidence!