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
- Go Testing Package - Official documentation
- Table Driven Tests - Dave Cheney
- Test Fixtures in Go - Dave Cheney
- Advanced Testing with Go - Mitchell Hashimoto
- Testify Package - Popular assertion library
- Go Mock - Mocking framework
- Testing Best Practices - Effective Go
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
-
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
-
Unit Testing
- Test individual functions in isolation
- Use Arrange-Act-Assert pattern
- Write descriptive test names
- Test both success and failure cases
-
Table-Driven Tests
- Reduce code duplication
- Easy to add new test cases
- Consistent test structure
- Encourage thorough testing
-
Mocking
- Mock at architectural boundaries
- Use interfaces for dependency injection
- Don't over-mock
- Verify interactions when needed
-
Benchmarking
- Measure before optimizing
- Compare different approaches
- Track memory allocations
- Benchmark on target hardware
-
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!