Go Testing and Benchmarking

Learning Objectives: By the end of this lesson, you will be able to:

  • Write comprehensive unit tests using Go’s testing package
  • Implement table-driven tests for thorough test coverage
  • Create and analyze benchmarks for performance optimization
  • Use testing tools like testify and generate mocks
  • Measure and improve code coverage
  • Profile applications to identify performance bottlenecks

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Testing is the safety net that allows developers to refactor fearlessly and deploy confidently. In Go, testing is built into the language itself, making it both powerful and approachable. Today we’ll master Go’s testing ecosystem and learn how to write tests that actually add value to your codebase.

Lesson Overview

  • Duration: 2-3 days (6-8 hours)
  • Prerequisites: Basic Go syntax, functions, structs, interfaces
  • Materials Needed: Go development environment, testing tools

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Unit Testing Fundamentals - Writing effective unit tests
  2. Table-Driven Tests - Testing multiple scenarios efficiently
  3. Test Organization - Structuring tests for maintainability
  4. Mocking and Interfaces - Testing with dependencies
  5. Benchmarking - Measuring and optimizing performance
  6. Integration Testing - Testing complete workflows
  7. Profiling - Identifying performance bottlenecks

Success Criteria:

By the end of this session, you should be able to write a comprehensive test suite that gives you confidence in your code’s correctness and performance.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Go’s Testing Philosophy

Go takes a minimalist approach to testing. The standard library’s testing package provides everything you need for unit tests, benchmarks, and examples without external dependencies.

Real-World Analogy: Think of testing like having a quality control process in manufacturing - you check each component individually (unit tests) and then test the assembled product (integration tests).

Why This Matters: Good tests serve as documentation, catch regressions, and enable confident refactoring. They’re an investment in code quality and developer productivity.

Test File Conventions

  • Test files end with _test.go
  • Test functions start with Test and take *testing.T
  • Benchmark functions start with Benchmark and take *testing.B
  • Example functions start with Example

Key Terminology

  • Unit Test: Tests a single function or method in isolation
  • Integration Test: Tests interaction between multiple components
  • Table-Driven Test: Uses a table of test cases to test multiple scenarios
  • Mock: Fake implementation used to isolate code under test
  • Coverage: Percentage of code executed by tests
  • Benchmark: Performance test that measures execution time

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Unit Testing

Unit testing in Go is straightforward and powerful. Let’s start with basic patterns and build up to more complex scenarios.

Basic Unit Testing Patterns

package calculator

import (
    "errors"
    "math"
)

// Calculator represents a basic calculator
type Calculator struct{}

// Add performs addition
func (c Calculator) Add(a, b float64) float64 {
    return a + b
}

// Subtract performs subtraction
func (c Calculator) Subtract(a, b float64) float64 {
    return a - b
}

// Multiply performs multiplication
func (c Calculator) Multiply(a, b float64) float64 {
    return a * b
}

// Divide performs division with error handling
func (c Calculator) Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Sqrt calculates square root
func (c Calculator) Sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, errors.New("cannot calculate square root of negative number")
    }
    return math.Sqrt(x), nil
}

// Power calculates a to the power of b
func (c Calculator) Power(a, b float64) float64 {
    return math.Pow(a, b)
}
package calculator

import (
    "math"
    "testing"
)

func TestCalculator_Add(t *testing.T) {
    calc := Calculator{}
    
    result := calc.Add(2, 3)
    expected := 5.0
    
    if result != expected {
        t.Errorf("Add(2, 3) = %v, want %v", result, expected)
    }
}

func TestCalculator_Subtract(t *testing.T) {
    calc := Calculator{}
    
    result := calc.Subtract(5, 3)
    expected := 2.0
    
    if result != expected {
        t.Errorf("Subtract(5, 3) = %v, want %v", result, expected)
    }
}

func TestCalculator_Multiply(t *testing.T) {
    calc := Calculator{}
    
    result := calc.Multiply(4, 3)
    expected := 12.0
    
    if result != expected {
        t.Errorf("Multiply(4, 3) = %v, want %v", result, expected)
    }
}

func TestCalculator_Divide(t *testing.T) {
    calc := Calculator{}
    
    // Test successful division
    result, err := calc.Divide(6, 2)
    if err != nil {
        t.Fatalf("Divide(6, 2) returned unexpected error: %v", err)
    }
    
    expected := 3.0
    if result != expected {
        t.Errorf("Divide(6, 2) = %v, want %v", result, expected)
    }
}

func TestCalculator_Divide_DivisionByZero(t *testing.T) {
    calc := Calculator{}
    
    _, err := calc.Divide(5, 0)
    if err == nil {
        t.Error("Divide(5, 0) should return an error, got nil")
    }
    
    expectedError := "division by zero"
    if err.Error() != expectedError {
        t.Errorf("Divide(5, 0) error = %v, want %v", err.Error(), expectedError)
    }
}

func TestCalculator_Sqrt(t *testing.T) {
    calc := Calculator{}
    
    // Test successful square root
    result, err := calc.Sqrt(9)
    if err != nil {
        t.Fatalf("Sqrt(9) returned unexpected error: %v", err)
    }
    
    expected := 3.0
    if result != expected {
        t.Errorf("Sqrt(9) = %v, want %v", result, expected)
    }
}

func TestCalculator_Sqrt_NegativeNumber(t *testing.T) {
    calc := Calculator{}
    
    _, err := calc.Sqrt(-4)
    if err == nil {
        t.Error("Sqrt(-4) should return an error, got nil")
    }
    
    expectedError := "cannot calculate square root of negative number"
    if err.Error() != expectedError {
        t.Errorf("Sqrt(-4) error = %v, want %v", err.Error(), expectedError)
    }
}

func TestCalculator_Power(t *testing.T) {
    calc := Calculator{}
    
    result := calc.Power(2, 3)
    expected := 8.0
    
    if result != expected {
        t.Errorf("Power(2, 3) = %v, want %v", result, expected)
    }
}

Table-Driven Tests

Table-driven tests are a Go idiom for testing multiple scenarios efficiently:

func TestCalculator_Add_TableDriven(t *testing.T) {
    calc := Calculator{}
    
    tests := []struct {
        name     string
        a, b     float64
        expected float64
    }{
        {
            name:     "positive numbers",
            a:        2.5,
            b:        3.7,
            expected: 6.2,
        },
        {
            name:     "negative numbers",
            a:        -5.0,
            b:        -3.0,
            expected: -8.0,
        },
        {
            name:     "mixed signs",
            a:        10.0,
            b:        -4.0,
            expected: 6.0,
        },
        {
            name:     "zero values",
            a:        0.0,
            b:        0.0,
            expected: 0.0,
        },
        {
            name:     "large numbers",
            a:        1000000.0,
            b:        2000000.0,
            expected: 3000000.0,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := calc.Add(tt.a, tt.b)
            if math.Abs(result-tt.expected) > 1e-9 {
                t.Errorf("Add(%v, %v) = %v, want %v", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

func TestCalculator_Divide_TableDriven(t *testing.T) {
    calc := Calculator{}
    
    tests := []struct {
        name        string
        a, b        float64
        expected    float64
        expectError bool
        errorMsg    string
    }{
        {
            name:        "normal division",
            a:           10.0,
            b:           2.0,
            expected:    5.0,
            expectError: false,
        },
        {
            name:        "division by zero",
            a:           5.0,
            b:           0.0,
            expectError: true,
            errorMsg:    "division by zero",
        },
        {
            name:        "negative result",
            a:           -15.0,
            b:           3.0,
            expected:    -5.0,
            expectError: false,
        },
        {
            name:        "decimal result",
            a:           7.0,
            b:           3.0,
            expected:    2.3333333333333335,
            expectError: false,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := calc.Divide(tt.a, tt.b)
            
            if tt.expectError {
                if err == nil {
                    t.Errorf("Divide(%v, %v) should return an error, got nil", tt.a, tt.b)
                    return
                }
                if err.Error() != tt.errorMsg {
                    t.Errorf("Divide(%v, %v) error = %v, want %v", tt.a, tt.b, err.Error(), tt.errorMsg)
                }
                return
            }
            
            if err != nil {
                t.Errorf("Divide(%v, %v) returned unexpected error: %v", tt.a, tt.b, err)
                return
            }
            
            if math.Abs(result-tt.expected) > 1e-9 {
                t.Errorf("Divide(%v, %v) = %v, want %v", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Testing with Dependencies and Mocks

package userservice

import (
    "errors"
    "fmt"
    "regexp"
    "strings"
)

// User represents a user in the system
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    FullName string `json:"full_name"`
}

// UserRepository defines the interface for user data access
type UserRepository interface {
    GetByID(id int) (*User, error)
    GetByUsername(username string) (*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(id int) error
}

// EmailService defines the interface for sending emails
type EmailService interface {
    SendWelcomeEmail(user *User) error
    SendPasswordResetEmail(user *User, token string) error
}

// UserService handles user business logic
type UserService struct {
    repo  UserRepository
    email EmailService
}

// NewUserService creates a new user service
func NewUserService(repo UserRepository, email EmailService) *UserService {
    return &UserService{
        repo:  repo,
        email: email,
    }
}

// ValidateUser validates user data
func (us *UserService) ValidateUser(user *User) error {
    if strings.TrimSpace(user.Username) == "" {
        return errors.New("username cannot be empty")
    }
    
    if len(user.Username) < 3 {
        return errors.New("username must be at least 3 characters")
    }
    
    if len(user.Username) > 20 {
        return errors.New("username cannot exceed 20 characters")
    }
    
    // Email validation
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(user.Email) {
        return errors.New("invalid email format")
    }
    
    if strings.TrimSpace(user.FullName) == "" {
        return errors.New("full name cannot be empty")
    }
    
    return nil
}

// CreateUser creates a new user and sends welcome email
func (us *UserService) CreateUser(user *User) error {
    // Validate user data
    if err := us.ValidateUser(user); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    // Check if username already exists
    existingUser, err := us.repo.GetByUsername(user.Username)
    if err == nil && existingUser != nil {
        return errors.New("username already exists")
    }
    
    // Create user
    if err := us.repo.Create(user); err != nil {
        return fmt.Errorf("failed to create user: %w", err)
    }
    
    // Send welcome email
    if err := us.email.SendWelcomeEmail(user); err != nil {
        // Log error but don't fail the operation
        // In a real app, you might want to queue this for retry
        fmt.Printf("Failed to send welcome email to %s: %v\n", user.Email, err)
    }
    
    return nil
}

// GetUser retrieves a user by ID
func (us *UserService) GetUser(id int) (*User, error) {
    if id <= 0 {
        return nil, errors.New("invalid user ID")
    }
    
    user, err := us.repo.GetByID(id)
    if err != nil {
        return nil, fmt.Errorf("failed to get user: %w", err)
    }
    
    return user, nil
}

// UpdateUser updates an existing user
func (us *UserService) UpdateUser(user *User) error {
    if user.ID <= 0 {
        return errors.New("invalid user ID")
    }
    
    // Validate user data
    if err := us.ValidateUser(user); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    // Check if user exists
    _, err := us.repo.GetByID(user.ID)
    if err != nil {
        return fmt.Errorf("user not found: %w", err)
    }
    
    // Update user
    if err := us.repo.Update(user); err != nil {
        return fmt.Errorf("failed to update user: %w", err)
    }
    
    return nil
}

Now let’s test this service with mocks:

package userservice

import (
    "errors"
    "testing"
)

// MockUserRepository implements UserRepository for testing
type MockUserRepository struct {
    users         map[int]*User
    usersByName   map[string]*User
    nextID        int
    shouldFailGet bool
    shouldFailCreate bool
    shouldFailUpdate bool
}

func NewMockUserRepository() *MockUserRepository {
    return &MockUserRepository{
        users:       make(map[int]*User),
        usersByName: make(map[string]*User),
        nextID:      1,
    }
}

func (m *MockUserRepository) GetByID(id int) (*User, error) {
    if m.shouldFailGet {
        return nil, errors.New("database error")
    }
    
    user, exists := m.users[id]
    if !exists {
        return nil, errors.New("user not found")
    }
    
    return user, nil
}

func (m *MockUserRepository) GetByUsername(username string) (*User, error) {
    if m.shouldFailGet {
        return nil, errors.New("database error")
    }
    
    user, exists := m.usersByName[username]
    if !exists {
        return nil, errors.New("user not found")
    }
    
    return user, nil
}

func (m *MockUserRepository) Create(user *User) error {
    if m.shouldFailCreate {
        return errors.New("database error")
    }
    
    user.ID = m.nextID
    m.nextID++
    
    m.users[user.ID] = user
    m.usersByName[user.Username] = user
    
    return nil
}

func (m *MockUserRepository) Update(user *User) error {
    if m.shouldFailUpdate {
        return errors.New("database error")
    }
    
    // Remove old username mapping
    if existingUser, exists := m.users[user.ID]; exists {
        delete(m.usersByName, existingUser.Username)
    }
    
    m.users[user.ID] = user
    m.usersByName[user.Username] = user
    
    return nil
}

func (m *MockUserRepository) Delete(id int) error {
    if user, exists := m.users[id]; exists {
        delete(m.users, id)
        delete(m.usersByName, user.Username)
    }
    return nil
}

// MockEmailService implements EmailService for testing
type MockEmailService struct {
    sentEmails       []string
    shouldFailWelcome bool
    shouldFailReset   bool
}

func NewMockEmailService() *MockEmailService {
    return &MockEmailService{
        sentEmails: make([]string, 0),
    }
}

func (m *MockEmailService) SendWelcomeEmail(user *User) error {
    if m.shouldFailWelcome {
        return errors.New("email service error")
    }
    
    m.sentEmails = append(m.sentEmails, "welcome:"+user.Email)
    return nil
}

func (m *MockEmailService) SendPasswordResetEmail(user *User, token string) error {
    if m.shouldFailReset {
        return errors.New("email service error")
    }
    
    m.sentEmails = append(m.sentEmails, "reset:"+user.Email+":"+token)
    return nil
}

func (m *MockEmailService) GetSentEmails() []string {
    return m.sentEmails
}

// Test cases using mocks
func TestUserService_ValidateUser(t *testing.T) {
    service := NewUserService(nil, nil) // No dependencies needed for validation
    
    tests := []struct {
        name        string
        user        *User
        expectError bool
        errorMsg    string
    }{
        {
            name: "valid user",
            user: &User{
                Username: "testuser",
                Email:    "test@example.com",
                FullName: "Test User",
            },
            expectError: false,
        },
        {
            name: "empty username",
            user: &User{
                Username: "",
                Email:    "test@example.com",
                FullName: "Test User",
            },
            expectError: true,
            errorMsg:    "username cannot be empty",
        },
        {
            name: "short username",
            user: &User{
                Username: "ab",
                Email:    "test@example.com",
                FullName: "Test User",
            },
            expectError: true,
            errorMsg:    "username must be at least 3 characters",
        },
        {
            name: "long username",
            user: &User{
                Username: "verylongusernamethatexceedslimit",
                Email:    "test@example.com",
                FullName: "Test User",
            },
            expectError: true,
            errorMsg:    "username cannot exceed 20 characters",
        },
        {
            name: "invalid email",
            user: &User{
                Username: "testuser",
                Email:    "invalid-email",
                FullName: "Test User",
            },
            expectError: true,
            errorMsg:    "invalid email format",
        },
        {
            name: "empty full name",
            user: &User{
                Username: "testuser",
                Email:    "test@example.com",
                FullName: "",
            },
            expectError: true,
            errorMsg:    "full name cannot be empty",
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := service.ValidateUser(tt.user)
            
            if tt.expectError {
                if err == nil {
                    t.Error("expected an error, got nil")
                    return
                }
                if err.Error() != tt.errorMsg {
                    t.Errorf("expected error %q, got %q", tt.errorMsg, err.Error())
                }
            } else {
                if err != nil {
                    t.Errorf("expected no error, got %v", err)
                }
            }
        })
    }
}

func TestUserService_CreateUser(t *testing.T) {
    mockRepo := NewMockUserRepository()
    mockEmail := NewMockEmailService()
    service := NewUserService(mockRepo, mockEmail)
    
    t.Run("successful creation", func(t *testing.T) {
        user := &User{
            Username: "newuser",
            Email:    "newuser@example.com",
            FullName: "New User",
        }
        
        err := service.CreateUser(user)
        if err != nil {
            t.Errorf("expected no error, got %v", err)
        }
        
        // Check user was created
        if user.ID == 0 {
            t.Error("user ID should be set after creation")
        }
        
        // Check welcome email was sent
        emails := mockEmail.GetSentEmails()
        if len(emails) != 1 {
            t.Errorf("expected 1 email, got %d", len(emails))
        }
        
        expectedEmail := "welcome:" + user.Email
        if emails[0] != expectedEmail {
            t.Errorf("expected email %q, got %q", expectedEmail, emails[0])
        }
    })
    
    t.Run("validation failure", func(t *testing.T) {
        user := &User{
            Username: "ab", // Too short
            Email:    "newuser@example.com",
            FullName: "New User",
        }
        
        err := service.CreateUser(user)
        if err == nil {
            t.Error("expected validation error, got nil")
        }
        
        // Check no emails were sent
        initialEmails := len(mockEmail.GetSentEmails())
        if len(mockEmail.GetSentEmails()) != initialEmails {
            t.Error("no emails should be sent for validation failure")
        }
    })
    
    t.Run("duplicate username", func(t *testing.T) {
        // Create first user
        user1 := &User{
            Username: "duplicate",
            Email:    "user1@example.com",
            FullName: "User One",
        }
        
        err := service.CreateUser(user1)
        if err != nil {
            t.Fatalf("failed to create first user: %v", err)
        }
        
        // Try to create second user with same username
        user2 := &User{
            Username: "duplicate",
            Email:    "user2@example.com",
            FullName: "User Two",
        }
        
        err = service.CreateUser(user2)
        if err == nil {
            t.Error("expected error for duplicate username, got nil")
        }
        
        if !strings.Contains(err.Error(), "username already exists") {
            t.Errorf("expected 'username already exists' error, got %v", err)
        }
    })
    
    t.Run("repository failure", func(t *testing.T) {
        mockRepo.shouldFailCreate = true
        defer func() { mockRepo.shouldFailCreate = false }()
        
        user := &User{
            Username: "failuser",
            Email:    "fail@example.com",
            FullName: "Fail User",
        }
        
        err := service.CreateUser(user)
        if err == nil {
            t.Error("expected repository error, got nil")
        }
        
        if !strings.Contains(err.Error(), "failed to create user") {
            t.Errorf("expected 'failed to create user' error, got %v", err)
        }
    })
}

func TestUserService_GetUser(t *testing.T) {
    mockRepo := NewMockUserRepository()
    service := NewUserService(mockRepo, nil)
    
    // Create a test user
    testUser := &User{
        Username: "testuser",
        Email:    "test@example.com",
        FullName: "Test User",
    }
    mockRepo.Create(testUser)
    
    t.Run("successful get", func(t *testing.T) {
        user, err := service.GetUser(testUser.ID)
        if err != nil {
            t.Errorf("expected no error, got %v", err)
        }
        
        if user.Username != testUser.Username {
            t.Errorf("expected username %q, got %q", testUser.Username, user.Username)
        }
    })
    
    t.Run("invalid ID", func(t *testing.T) {
        _, err := service.GetUser(0)
        if err == nil {
            t.Error("expected error for invalid ID, got nil")
        }
        
        if err.Error() != "invalid user ID" {
            t.Errorf("expected 'invalid user ID' error, got %v", err)
        }
    })
    
    t.Run("user not found", func(t *testing.T) {
        _, err := service.GetUser(999)
        if err == nil {
            t.Error("expected error for non-existent user, got nil")
        }
        
        if !strings.Contains(err.Error(), "failed to get user") {
            t.Errorf("expected 'failed to get user' error, got %v", err)
        }
    })
}

5. Live Coding Demonstration (20-25 minutes)

Example 1: Benchmarking Performance

package performance

import (
    "fmt"
    "strings"
    "testing"
)

// Different string concatenation methods to benchmark
func ConcatWithOperator(strs []string) string {
    result := ""
    for _, s := range strs {
        result += s
    }
    return result
}

func ConcatWithBuilder(strs []string) string {
    var builder strings.Builder
    for _, s := range strs {
        builder.WriteString(s)
    }
    return builder.String()
}

func ConcatWithPreAlloc(strs []string) string {
    // Pre-calculate total length
    totalLen := 0
    for _, s := range strs {
        totalLen += len(s)
    }
    
    var builder strings.Builder
    builder.Grow(totalLen) // Pre-allocate
    
    for _, s := range strs {
        builder.WriteString(s)
    }
    return builder.String()
}

func ConcatWithJoin(strs []string) string {
    return strings.Join(strs, "")
}

// Benchmark functions
func BenchmarkConcatWithOperator(b *testing.B) {
    strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ConcatWithOperator(strs)
    }
}

func BenchmarkConcatWithBuilder(b *testing.B) {
    strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ConcatWithBuilder(strs)
    }
}

func BenchmarkConcatWithPreAlloc(b *testing.B) {
    strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ConcatWithPreAlloc(strs)
    }
}

func BenchmarkConcatWithJoin(b *testing.B) {
    strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ConcatWithJoin(strs)
    }
}

// Benchmark with different sizes
func BenchmarkConcatSmall(b *testing.B) {
    strs := []string{"a", "b", "c"}
    
    b.Run("Operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithOperator(strs)
        }
    })
    
    b.Run("Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithBuilder(strs)
        }
    })
    
    b.Run("Join", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithJoin(strs)
        }
    })
}

func BenchmarkConcatLarge(b *testing.B) {
    strs := make([]string, 1000)
    for i := range strs {
        strs[i] = fmt.Sprintf("string_%d", i)
    }
    
    b.Run("Operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithOperator(strs)
        }
    })
    
    b.Run("Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithBuilder(strs)
        }
    })
    
    b.Run("PreAlloc", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithPreAlloc(strs)
        }
    })
    
    b.Run("Join", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            ConcatWithJoin(strs)
        }
    })
}

// Memory allocation benchmarks
func BenchmarkConcatMemory(b *testing.B) {
    strs := make([]string, 100)
    for i := range strs {
        strs[i] = fmt.Sprintf("str_%d", i)
    }
    
    b.Run("Operator", func(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            ConcatWithOperator(strs)
        }
    })
    
    b.Run("Builder", func(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            ConcatWithBuilder(strs)
        }
    })
    
    b.Run("PreAlloc", func(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            ConcatWithPreAlloc(strs)
        }
    })
}

Example 2: Integration Testing with HTTP Handlers

package api

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    
    "github.com/gorilla/mux"
)

type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    FullName string `json:"full_name"`
}

type UserAPI struct {
    service UserService
}

func NewUserAPI(service UserService) *UserAPI {
    return &UserAPI{service: service}
}

func (api *UserAPI) GetUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    idStr := vars["id"]
    
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }
    
    user, err := api.service.GetUser(id)
    if err != nil {
        if strings.Contains(err.Error(), "not found") {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func (api *UserAPI) CreateUser(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    if err := api.service.CreateUser(&user); err != nil {
        if strings.Contains(err.Error(), "validation") {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        if strings.Contains(err.Error(), "already exists") {
            http.Error(w, err.Error(), http.StatusConflict)
            return
        }
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func (api *UserAPI) UpdateUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    idStr := vars["id"]
    
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }
    
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    user.ID = id
    
    if err := api.service.UpdateUser(&user); err != nil {
        if strings.Contains(err.Error(), "validation") {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        if strings.Contains(err.Error(), "not found") {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func (api *UserAPI) SetupRoutes() http.Handler {
    r := mux.NewRouter()
    
    r.HandleFunc("/users/{id:[0-9]+}", api.GetUser).Methods("GET")
    r.HandleFunc("/users", api.CreateUser).Methods("POST")
    r.HandleFunc("/users/{id:[0-9]+}", api.UpdateUser).Methods("PUT")
    
    return r
}
package api

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserAPI_GetUser(t *testing.T) {
    mockRepo := NewMockUserRepository()
    mockEmail := NewMockEmailService()
    service := NewUserService(mockRepo, mockEmail)
    api := NewUserAPI(service)
    
    // Create test user
    testUser := &User{
        Username: "testuser",
        Email:    "test@example.com",
        FullName: "Test User",
    }
    service.CreateUser(testUser)
    
    router := api.SetupRoutes()
    
    t.Run("successful get", func(t *testing.T) {
        req := httptest.NewRequest("GET", fmt.Sprintf("/users/%d", testUser.ID), nil)
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusOK {
            t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
        }
        
        var response User
        err := json.NewDecoder(w.Body).Decode(&response)
        if err != nil {
            t.Fatalf("failed to decode response: %v", err)
        }
        
        if response.Username != testUser.Username {
            t.Errorf("expected username %q, got %q", testUser.Username, response.Username)
        }
    })
    
    t.Run("user not found", func(t *testing.T) {
        req := httptest.NewRequest("GET", "/users/999", nil)
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusNotFound {
            t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
        }
    })
    
    t.Run("invalid user ID", func(t *testing.T) {
        req := httptest.NewRequest("GET", "/users/invalid", nil)
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusNotFound { // Mux will return 404 for non-matching route
            t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
        }
    })
}

func TestUserAPI_CreateUser(t *testing.T) {
    mockRepo := NewMockUserRepository()
    mockEmail := NewMockEmailService()
    service := NewUserService(mockRepo, mockEmail)
    api := NewUserAPI(service)
    
    router := api.SetupRoutes()
    
    t.Run("successful creation", func(t *testing.T) {
        user := User{
            Username: "newuser",
            Email:    "new@example.com",
            FullName: "New User",
        }
        
        body, _ := json.Marshal(user)
        req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusCreated {
            t.Errorf("expected status %d, got %d", http.StatusCreated, w.Code)
        }
        
        var response User
        err := json.NewDecoder(w.Body).Decode(&response)
        if err != nil {
            t.Fatalf("failed to decode response: %v", err)
        }
        
        if response.ID == 0 {
            t.Error("expected user ID to be set")
        }
        
        if response.Username != user.Username {
            t.Errorf("expected username %q, got %q", user.Username, response.Username)
        }
    })
    
    t.Run("validation error", func(t *testing.T) {
        user := User{
            Username: "ab", // Too short
            Email:    "new@example.com",
            FullName: "New User",
        }
        
        body, _ := json.Marshal(user)
        req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusBadRequest {
            t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
        }
    })
    
    t.Run("invalid JSON", func(t *testing.T) {
        req := httptest.NewRequest("POST", "/users", bytes.NewReader([]byte("invalid json")))
        req.Header.Set("Content-Type", "application/json")
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusBadRequest {
            t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
        }
    })
}

func TestUserAPI_UpdateUser(t *testing.T) {
    mockRepo := NewMockUserRepository()
    mockEmail := NewMockEmailService()
    service := NewUserService(mockRepo, mockEmail)
    api := NewUserAPI(service)
    
    // Create test user
    testUser := &User{
        Username: "testuser",
        Email:    "test@example.com",
        FullName: "Test User",
    }
    service.CreateUser(testUser)
    
    router := api.SetupRoutes()
    
    t.Run("successful update", func(t *testing.T) {
        updatedUser := User{
            Username: "updateduser",
            Email:    "updated@example.com",
            FullName: "Updated User",
        }
        
        body, _ := json.Marshal(updatedUser)
        req := httptest.NewRequest("PUT", fmt.Sprintf("/users/%d", testUser.ID), bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusOK {
            t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
        }
        
        var response User
        err := json.NewDecoder(w.Body).Decode(&response)
        if err != nil {
            t.Fatalf("failed to decode response: %v", err)
        }
        
        if response.Username != updatedUser.Username {
            t.Errorf("expected username %q, got %q", updatedUser.Username, response.Username)
        }
    })
    
    t.Run("user not found", func(t *testing.T) {
        user := User{
            Username: "notfound",
            Email:    "notfound@example.com",
            FullName: "Not Found",
        }
        
        body, _ := json.Marshal(user)
        req := httptest.NewRequest("PUT", "/users/999", bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusNotFound {
            t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
        }
    })
}

// Benchmark API endpoints
func BenchmarkUserAPI_GetUser(b *testing.B) {
    mockRepo := NewMockUserRepository()
    mockEmail := NewMockEmailService()
    service := NewUserService(mockRepo, mockEmail)
    api := NewUserAPI(service)
    
    // Create test user
    testUser := &User{
        Username: "benchuser",
        Email:    "bench@example.com",
        FullName: "Bench User",
    }
    service.CreateUser(testUser)
    
    router := api.SetupRoutes()
    
    b.ResetTimer()
    b.ReportAllocs()
    
    for i := 0; i < b.N; i++ {
        req := httptest.NewRequest("GET", fmt.Sprintf("/users/%d", testUser.ID), nil)
        w := httptest.NewRecorder()
        
        router.ServeHTTP(w, req)
        
        if w.Code != http.StatusOK {
            b.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
        }
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: E-commerce Testing Suite (10 minutes)

Objective: Write comprehensive tests for a shopping cart system

Instructions:

  1. Create unit tests for cart operations (add, remove, update quantities)
  2. Write table-driven tests for price calculations
  3. Test edge cases like empty carts and invalid items
  4. Benchmark different discount calculation algorithms

Starter Code:

type ShoppingCart struct {
    Items []CartItem
}

type CartItem struct {
    ProductID int
    Quantity  int
    Price     float64
}

func (c *ShoppingCart) AddItem(productID int, quantity int, price float64) error {
    // TODO: Implement
    return nil
}

func (c *ShoppingCart) GetTotal() float64 {
    // TODO: Implement
    return 0.0
}

func TestShoppingCart_AddItem(t *testing.T) {
    // TODO: Write comprehensive tests
}

Exercise 2: HTTP Client Testing (15 minutes)

Objective: Test an HTTP client that consumes external APIs

Requirements:

  • Mock HTTP responses using httptest
  • Test different status codes and error conditions
  • Verify request headers and payloads
  • Test timeout and retry logic

Hints:

  • Use httptest.NewServer for creating test servers
  • Test both success and failure scenarios
  • Consider using testify/assert for cleaner assertions

Exercise 3: Concurrent Code Testing (15 minutes)

Objective: Test concurrent operations safely

Challenge: Write tests for:

  • Race condition detection
  • Goroutine leak prevention
  • Channel communication patterns
  • Worker pool implementations

Extension Ideas:

  • Use go test -race to detect race conditions
  • Implement stress tests with multiple goroutines
  • Test graceful shutdown scenarios

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Tests that depend on external services Cause: Not properly mocking dependencies Solution: Use interfaces and dependency injection
// Wrong - depends on external database
func TestUserService_GetUser(t *testing.T) {
    db, _ := sql.Open("postgres", "host=localhost...")
    service := NewUserService(db)
    // Test will fail if database is unavailable
}

// Right - uses mock
func TestUserService_GetUser(t *testing.T) {
    mockRepo := NewMockUserRepository()
    service := NewUserService(mockRepo)
    // Test is isolated and reliable
}
  1. Problem: Non-deterministic tests Cause: Depending on time, random values, or order of operations Solution: Control inputs and use fixed values
// Wrong - depends on current time
func TestExpiration(t *testing.T) {
    token := CreateToken(time.Now().Add(time.Hour))
    if token.IsExpired() {
        t.Error("token should not be expired")
    }
}

// Right - uses fixed time
func TestExpiration(t *testing.T) {
    fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
    token := CreateToken(fixedTime.Add(time.Hour))
    if token.IsExpiredAt(fixedTime) {
        t.Error("token should not be expired")
    }
}

Debugging Tips

  • Use t.Helper() in test helper functions
  • Run tests with -v flag for verbose output
  • Use t.Parallel() for independent tests
  • Consider using testify for better assertions and test setup

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Test Naming: Use descriptive names that explain the scenario
  • Test Organization: Group related tests using subtests
  • Coverage: Aim for meaningful coverage, not just high percentages
  • Documentation: Write tests that serve as documentation

Performance Testing

func BenchmarkFunction(b *testing.B) {
    // Setup code here (not timed)
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        // Code to benchmark
        result := ExpensiveFunction()
        _ = result // Prevent optimization
    }
}

Test Coverage Analysis

# Run tests with coverage
go test -cover

# Generate detailed coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

Next Steps in Your Learning Journey

  • Learn property-based testing with tools like gopter
  • Explore mutation testing for test quality assessment
  • Study advanced profiling techniques
  • Practice with real-world testing scenarios

9. Summary and Review (5 minutes)

Key Takeaways

  1. Go’s testing package provides powerful tools for unit tests, benchmarks, and examples
  2. Table-driven tests enable comprehensive scenario coverage efficiently
  3. Mocking is essential for isolated, reliable unit tests
  4. Benchmarking helps identify and fix performance issues

Quick Review Questions

  1. What’s the difference between t.Error and t.Fatal?
  2. How do you structure table-driven tests effectively?
  3. When should you use benchmarks vs. profiling?

For Instructors

Teaching Tips

  • Demonstrate test-driven development (TDD) workflow
  • Show both positive and negative test cases
  • Emphasize the importance of test maintainability
  • Use real examples from production code

Assessment Ideas

  • Build a complete test suite for a web service
  • Implement performance optimization based on benchmark results
  • Create integration tests for a database-backed application

Adaptation Notes

  • For beginners: Focus on unit testing fundamentals and basic benchmarking
  • For advanced students: Include property-based testing and advanced profiling
  • Online delivery: Provide sample codebases for testing practice