Advanced Testing Techniques

Why This Matters - Testing in the Real World

In 2018, a major cloud service outage cost an estimated $150 million in revenue when a database driver update broke existing mocks. In 2019, a financial services firm lost $2.4 million when contract testing failures allowed incompatible API changes to reach production. Advanced testing isn't optional—it's your defense against distributed system failures.

The Real Cost of Testing Gaps:

  • Microservice integration failures: Average $300K per incident
  • API compatibility breaks: 62% cause production downtime
  • Performance regressions: 45% detected only after user complaints
  • Contract violations: 78% lead to cascade failures across services

Advanced testing techniques create controlled environments for testing complex interactions. Like building test tracks for race cars, we create predictable scenarios to verify behavior without real-world risks, ensuring our distributed systems work reliably under all conditions.

Learning Objectives

By the end of this article, you will master:

🎯 Advanced Testing Concepts

  • Interface-based mocking principles and dependency injection
  • Contract testing for distributed systems reliability
  • Time and database mocking strategies

🔧 Practical Mocking Skills

  • Creating sophisticated test doubles and stubs
  • HTTP testing with httptest for API reliability
  • State management in mocks for complex scenarios

🏗️ Production Testing Patterns

  • Contract testing for service integration
  • Schema validation and API compatibility testing
  • Performance and chaos testing techniques

Core Concepts - Interface-Based Mocking

Interfaces are the foundation of mocking in Go—they define contracts without prescribing implementation. This separation enables dependency injection and test isolation.

The Dependency Inversion Principle

Always depend on abstractions, not concretions. This fundamental principle makes your code testable and flexible.

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9// EmailSender interface enables mocking
10type EmailSender interface {
11    Send(to, subject, body string) error
12}
13
14// Production implementation
15type RealEmailSender struct {
16    SMTPHost string
17}
18
19func (s *RealEmailSender) Send(to, subject, body string) error {
20    // In production, this would use SMTP
21    return fmt.Errorf("SMTP not configured")
22}
23
24// Mock for testing
25type MockEmailSender struct {
26    SentEmails []SentEmail
27    SendError  error
28}
29
30type SentEmail struct {
31    To      string
32    Subject string
33    Body    string
34    SentAt  time.Time
35}
36
37func (m *MockEmailSender) Send(to, subject, body string) error {
38    if m.SendError != nil {
39        return m.SendError
40    }
41
42    m.SentEmails = append(m.SentEmails, SentEmail{
43        To:      to,
44        Subject: subject,
45        Body:    body,
46        SentAt:  time.Now(),
47    })
48    return nil
49}
50
51// Service using the interface
52type UserService struct {
53    emailSender EmailSender
54}
55
56func NewUserService(sender EmailSender) *UserService {
57    return &UserService{emailSender: sender}
58}
59
60func (s *UserService) RegisterUser(email, name string) error {
61    subject := "Welcome!"
62    body := fmt.Sprintf("Hello %s, welcome to our service!", name)
63    return s.emailSender.Send(email, subject, body)
64}
65
66func main() {
67    mock := &MockEmailSender{}
68    service := NewUserService(mock)
69
70    err := service.RegisterUser("alice@example.com", "Alice")
71    if err != nil {
72        fmt.Println("Error:", err)
73        return
74    }
75
76    fmt.Printf("Sent %d emails\n", len(mock.SentEmails))
77    if len(mock.SentEmails) > 0 {
78        fmt.Printf("First email to: %s\n", mock.SentEmails[0].To)
79    }
80}

Advanced Mock Patterns

Stateful Mocks maintain internal state:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6)
 7
 8type User struct {
 9    ID    int
10    Name  string
11    Email string
12}
13
14type StatefulDatabase struct {
15    users    map[int]*User
16    nextID   int
17    failures map[string]bool
18}
19
20func NewStatefulDatabase() *StatefulDatabase {
21    return &StatefulDatabase{
22        users:    make(map[int]*User),
23        nextID:   1,
24        failures: make(map[string]bool),
25    }
26}
27
28func (db *StatefulDatabase) SaveUser(user *User) (int, error) {
29    if db.failures["save"] {
30        return 0, fmt.Errorf("database save failed")
31    }
32
33    id := db.nextID
34    user.ID = id
35    db.users[id] = user
36    db.nextID++
37    return id, nil
38}
39
40func (db *StatefulDatabase) GetUser(id int) (*User, error) {
41    if db.failures["get"] {
42        return nil, fmt.Errorf("database get failed")
43    }
44
45    user, exists := db.users[id]
46    if !exists {
47        return nil, fmt.Errorf("user not found")
48    }
49    return user, nil
50}
51
52func (db *StatefulDatabase) SetFailure(operation string, fail bool) {
53    db.failures[operation] = fail
54}
55
56func main() {
57    db := NewStatefulDatabase()
58
59    user := &User{Name: "Alice", Email: "alice@example.com"}
60    id, err := db.SaveUser(user)
61    if err != nil {
62        fmt.Println("Error:", err)
63        return
64    }
65
66    fmt.Printf("Saved user with ID: %d\n", id)
67
68    retrieved, err := db.GetUser(id)
69    if err != nil {
70        fmt.Println("Error:", err)
71        return
72    }
73
74    fmt.Printf("Retrieved user: %s\n", retrieved.Name)
75
76    // Test failure scenario
77    db.SetFailure("get", true)
78    _, err = db.GetUser(id)
79    fmt.Printf("Expected failure: %v\n", err)
80}

Conditional Mocks behave based on input:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "strings"
 7)
 8
 9type ConditionalEmailSender struct {
10    sendFunc     func(to, subject, body string) error
11    blockedEmails map[string]bool
12}
13
14func NewConditionalEmailSender() *ConditionalEmailSender {
15    return &ConditionalEmailSender{
16        blockedEmails: make(map[string]bool),
17    }
18}
19
20func (m *ConditionalEmailSender) BlockEmail(email string) {
21    m.blockedEmails[email] = true
22}
23
24func (m *ConditionalEmailSender) SetSendFunc(f func(to, subject, body string) error) {
25    m.sendFunc = f
26}
27
28func (m *ConditionalEmailSender) Send(to, subject, body string) error {
29    if m.sendFunc != nil {
30        return m.sendFunc(to, subject, body)
31    }
32
33    if m.blockedEmails[to] {
34        return fmt.Errorf("email blocked: %s", to)
35    }
36
37    if strings.Contains(subject, "spam") {
38        return fmt.Errorf("spam detected in subject")
39    }
40
41    return nil
42}
43
44func main() {
45    sender := NewConditionalEmailSender()
46
47    // Normal email
48    err := sender.Send("alice@example.com", "Welcome", "Hello!")
49    fmt.Printf("Normal send: %v\n", err)
50
51    // Blocked email
52    sender.BlockEmail("blocked@example.com")
53    err = sender.Send("blocked@example.com", "Welcome", "Hello!")
54    fmt.Printf("Blocked send: %v\n", err)
55
56    // Spam detection
57    err = sender.Send("user@example.com", "spam alert", "Hello!")
58    fmt.Printf("Spam send: %v\n", err)
59}

Spy Pattern for Behavior Verification

Spy mocks record all interactions for later verification:

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9type PaymentGateway interface {
10    ProcessPayment(amount float64) error
11    RefundPayment(transactionID string) error
12}
13
14type PaymentSpy struct {
15    ProcessPaymentCalls []PaymentCall
16    RefundPaymentCalls  []RefundCall
17}
18
19type PaymentCall struct {
20    Amount    float64
21    Timestamp time.Time
22}
23
24type RefundCall struct {
25    TransactionID string
26    Timestamp     time.Time
27}
28
29func NewPaymentSpy() *PaymentSpy {
30    return &PaymentSpy{
31        ProcessPaymentCalls: make([]PaymentCall, 0),
32        RefundPaymentCalls:  make([]RefundCall, 0),
33    }
34}
35
36func (s *PaymentSpy) ProcessPayment(amount float64) error {
37    s.ProcessPaymentCalls = append(s.ProcessPaymentCalls, PaymentCall{
38        Amount:    amount,
39        Timestamp: time.Now(),
40    })
41    return nil
42}
43
44func (s *PaymentSpy) RefundPayment(transactionID string) error {
45    s.RefundPaymentCalls = append(s.RefundPaymentCalls, RefundCall{
46        TransactionID: transactionID,
47        Timestamp:     time.Now(),
48    })
49    return nil
50}
51
52func (s *PaymentSpy) AssertProcessPaymentCalledWith(amount float64) bool {
53    for _, call := range s.ProcessPaymentCalls {
54        if call.Amount == amount {
55            return true
56        }
57    }
58    return false
59}
60
61func (s *PaymentSpy) AssertRefundPaymentCalledWith(txID string) bool {
62    for _, call := range s.RefundPaymentCalls {
63        if call.TransactionID == txID {
64            return true
65        }
66    }
67    return false
68}
69
70func main() {
71    spy := NewPaymentSpy()
72
73    // Make some calls
74    spy.ProcessPayment(100.50)
75    spy.ProcessPayment(250.00)
76    spy.RefundPayment("tx-123")
77
78    // Verify behavior
79    fmt.Printf("ProcessPayment called %d times\n", len(spy.ProcessPaymentCalls))
80    fmt.Printf("RefundPayment called %d times\n", len(spy.RefundPaymentCalls))
81
82    fmt.Printf("ProcessPayment(100.50) called: %v\n",
83        spy.AssertProcessPaymentCalledWith(100.50))
84    fmt.Printf("RefundPayment(tx-123) called: %v\n",
85        spy.AssertRefundPaymentCalledWith("tx-123"))
86}

HTTP Mocking

HTTP mocking creates controlled API environments for testing external service interactions.

httptest.Server: Complete Server Simulation

  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "io"
  8    "net/http"
  9    "net/http/httptest"
 10)
 11
 12type APIClient struct {
 13    baseURL string
 14    client  *http.Client
 15}
 16
 17func NewAPIClient(baseURL string) *APIClient {
 18    return &APIClient{
 19        baseURL: baseURL,
 20        client:  &http.Client{},
 21    }
 22}
 23
 24type User struct {
 25    ID    int    `json:"id"`
 26    Name  string `json:"name"`
 27    Email string `json:"email"`
 28}
 29
 30func (c *APIClient) GetUser(id int) (*User, error) {
 31    url := fmt.Sprintf("%s/api/users/%d", c.baseURL, id)
 32    resp, err := c.client.Get(url)
 33    if err != nil {
 34        return nil, err
 35    }
 36    defer resp.Body.Close()
 37
 38    if resp.StatusCode != http.StatusOK {
 39        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
 40    }
 41
 42    var user User
 43    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
 44        return nil, err
 45    }
 46
 47    return &user, nil
 48}
 49
 50func (c *APIClient) CreateUser(user *User) (*User, error) {
 51    url := fmt.Sprintf("%s/api/users", c.baseURL)
 52
 53    body, err := json.Marshal(user)
 54    if err != nil {
 55        return nil, err
 56    }
 57
 58    resp, err := c.client.Post(url, "application/json", io.NopCloser(json.NewDecoder(body)))
 59    if err != nil {
 60        return nil, err
 61    }
 62    defer resp.Body.Close()
 63
 64    if resp.StatusCode != http.StatusCreated {
 65        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
 66    }
 67
 68    var created User
 69    if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
 70        return nil, err
 71    }
 72
 73    return &created, nil
 74}
 75
 76func main() {
 77    // Create mock server
 78    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 79        switch {
 80        case r.Method == "GET" && r.URL.Path == "/api/users/123":
 81            user := User{ID: 123, Name: "Alice", Email: "alice@example.com"}
 82            w.Header().Set("Content-Type", "application/json")
 83            json.NewEncoder(w).Encode(user)
 84
 85        case r.Method == "POST" && r.URL.Path == "/api/users":
 86            var user User
 87            json.NewDecoder(r.Body).Decode(&user)
 88            user.ID = 456
 89            w.Header().Set("Content-Type", "application/json")
 90            w.WriteHeader(http.StatusCreated)
 91            json.NewEncoder(w).Encode(user)
 92
 93        default:
 94            w.WriteHeader(http.StatusNotFound)
 95        }
 96    }))
 97    defer server.Close()
 98
 99    // Use client with mock server
100    client := NewAPIClient(server.URL)
101
102    user, err := client.GetUser(123)
103    if err != nil {
104        fmt.Println("Error:", err)
105        return
106    }
107    fmt.Printf("Retrieved user: %s (%s)\n", user.Name, user.Email)
108}

Advanced HTTP Mocking Patterns

Request Matching and Assertion:

 1// run
 2package main
 3
 4import (
 5    "encoding/json"
 6    "fmt"
 7    "io"
 8    "net/http"
 9    "net/http/httptest"
10    "strings"
11)
12
13type HTTPMockServer struct {
14    server          *httptest.Server
15    receivedRequests []HTTPRequest
16}
17
18type HTTPRequest struct {
19    Method  string
20    Path    string
21    Headers map[string]string
22    Body    string
23}
24
25func NewHTTPMockServer() *HTTPMockServer {
26    mock := &HTTPMockServer{
27        receivedRequests: make([]HTTPRequest, 0),
28    }
29
30    mock.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31        // Record request
32        body, _ := io.ReadAll(r.Body)
33        headers := make(map[string]string)
34        for key, values := range r.Header {
35            headers[key] = strings.Join(values, ",")
36        }
37
38        mock.receivedRequests = append(mock.receivedRequests, HTTPRequest{
39            Method:  r.Method,
40            Path:    r.URL.Path,
41            Headers: headers,
42            Body:    string(body),
43        })
44
45        // Respond
46        w.Header().Set("Content-Type", "application/json")
47        json.NewEncoder(w).Encode(map[string]string{
48            "status": "ok",
49            "path":   r.URL.Path,
50        })
51    }))
52
53    return mock
54}
55
56func (m *HTTPMockServer) Close() {
57    m.server.Close()
58}
59
60func (m *HTTPMockServer) URL() string {
61    return m.server.URL
62}
63
64func (m *HTTPMockServer) GetReceivedRequests() []HTTPRequest {
65    return m.receivedRequests
66}
67
68func (m *HTTPMockServer) AssertRequestMade(method, path string) bool {
69    for _, req := range m.receivedRequests {
70        if req.Method == method && req.Path == path {
71            return true
72        }
73    }
74    return false
75}
76
77func main() {
78    mock := NewHTTPMockServer()
79    defer mock.Close()
80
81    // Make some requests
82    http.Get(mock.URL() + "/api/users")
83    http.Post(mock.URL()+"/api/login", "application/json",
84        strings.NewReader(`{"username":"alice"}`))
85
86    // Verify requests
87    fmt.Printf("Received %d requests\n", len(mock.GetReceivedRequests()))
88    fmt.Printf("GET /api/users called: %v\n",
89        mock.AssertRequestMade("GET", "/api/users"))
90    fmt.Printf("POST /api/login called: %v\n",
91        mock.AssertRequestMade("POST", "/api/login"))
92}

Time Mocking

Time mocking eliminates waiting in time-dependent tests by controlling the clock.

Clock Interface Pattern

 1// run
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9type Clock interface {
10    Now() time.Time
11    Sleep(d time.Duration)
12    After(d time.Duration) <-chan time.Time
13}
14
15type RealClock struct{}
16
17func (RealClock) Now() time.Time                    { return time.Now() }
18func (RealClock) Sleep(d time.Duration)             { time.Sleep(d) }
19func (RealClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
20
21type MockClock struct {
22    currentTime time.Time
23    afterChans  []chan time.Time
24}
25
26func NewMockClock(start time.Time) *MockClock {
27    return &MockClock{
28        currentTime: start,
29        afterChans:  make([]chan time.Time, 0),
30    }
31}
32
33func (c *MockClock) Now() time.Time {
34    return c.currentTime
35}
36
37func (c *MockClock) Sleep(d time.Duration) {
38    c.Advance(d)
39}
40
41func (c *MockClock) After(d time.Duration) <-chan time.Time {
42    ch := make(chan time.Time, 1)
43    c.afterChans = append(c.afterChans, ch)
44    return ch
45}
46
47func (c *MockClock) Advance(d time.Duration) {
48    c.currentTime = c.currentTime.Add(d)
49
50    // Trigger any expired After channels
51    for _, ch := range c.afterChans {
52        select {
53        case ch <- c.currentTime:
54        default:
55        }
56    }
57}
58
59func (c *MockClock) SetTime(t time.Time) {
60    c.currentTime = t
61}
62
63func main() {
64    // Use mock clock
65    clock := NewMockClock(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC))
66
67    fmt.Printf("Start time: %v\n", clock.Now())
68
69    clock.Advance(time.Hour)
70    fmt.Printf("After 1 hour: %v\n", clock.Now())
71
72    clock.Advance(24 * time.Hour)
73    fmt.Printf("After 1 day: %v\n", clock.Now())
74}

Session Manager Example with Time Mocking

  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "sync"
  7    "time"
  8)
  9
 10type SessionManager struct {
 11    clock    Clock
 12    sessions map[string]*Session
 13    mu       sync.RWMutex
 14}
 15
 16type Session struct {
 17    ID        string
 18    UserID    int
 19    ExpiresAt time.Time
 20    Data      map[string]interface{}
 21}
 22
 23func NewSessionManager(clock Clock) *SessionManager {
 24    return &SessionManager{
 25        clock:    clock,
 26        sessions: make(map[string]*Session),
 27    }
 28}
 29
 30func (sm *SessionManager) CreateSession(userID int, ttl time.Duration) string {
 31    sm.mu.Lock()
 32    defer sm.mu.Unlock()
 33
 34    sessionID := fmt.Sprintf("session-%d-%d", userID, time.Now().UnixNano())
 35    session := &Session{
 36        ID:        sessionID,
 37        UserID:    userID,
 38        ExpiresAt: sm.clock.Now().Add(ttl),
 39        Data:      make(map[string]interface{}),
 40    }
 41
 42    sm.sessions[sessionID] = session
 43    return sessionID
 44}
 45
 46func (sm *SessionManager) IsValid(sessionID string) bool {
 47    sm.mu.RLock()
 48    defer sm.mu.RUnlock()
 49
 50    session, exists := sm.sessions[sessionID]
 51    if !exists {
 52        return false
 53    }
 54
 55    return sm.clock.Now().Before(session.ExpiresAt)
 56}
 57
 58func (sm *SessionManager) GetSession(sessionID string) (*Session, bool) {
 59    sm.mu.RLock()
 60    defer sm.mu.RUnlock()
 61
 62    session, exists := sm.sessions[sessionID]
 63    if !exists {
 64        return nil, false
 65    }
 66
 67    if sm.clock.Now().After(session.ExpiresAt) {
 68        return nil, false
 69    }
 70
 71    return session, true
 72}
 73
 74func (sm *SessionManager) CleanupExpired() int {
 75    sm.mu.Lock()
 76    defer sm.mu.Unlock()
 77
 78    count := 0
 79    now := sm.clock.Now()
 80
 81    for id, session := range sm.sessions {
 82        if now.After(session.ExpiresAt) {
 83            delete(sm.sessions, id)
 84            count++
 85        }
 86    }
 87
 88    return count
 89}
 90
 91type Clock interface {
 92    Now() time.Time
 93    Sleep(d time.Duration)
 94    After(d time.Duration) <-chan time.Time
 95}
 96
 97type MockClock struct {
 98    currentTime time.Time
 99}
100
101func NewMockClock(start time.Time) *MockClock {
102    return &MockClock{currentTime: start}
103}
104
105func (c *MockClock) Now() time.Time { return c.currentTime }
106func (c *MockClock) Sleep(d time.Duration) { c.currentTime = c.currentTime.Add(d) }
107func (c *MockClock) After(d time.Duration) <-chan time.Time {
108    ch := make(chan time.Time, 1)
109    ch <- c.currentTime.Add(d)
110    return ch
111}
112func (c *MockClock) Advance(d time.Duration) { c.currentTime = c.currentTime.Add(d) }
113
114func main() {
115    mockClock := NewMockClock(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC))
116    sm := NewSessionManager(mockClock)
117
118    // Create session
119    sessionID := sm.CreateSession(123, time.Hour)
120    fmt.Printf("Created session: %s\n", sessionID)
121
122    // Immediately valid
123    fmt.Printf("Valid after creation: %v\n", sm.IsValid(sessionID))
124
125    // Advance 30 minutes
126    mockClock.Advance(30 * time.Minute)
127    fmt.Printf("Valid after 30 minutes: %v\n", sm.IsValid(sessionID))
128
129    // Advance 45 more minutes (total 75 minutes)
130    mockClock.Advance(45 * time.Minute)
131    fmt.Printf("Valid after 75 minutes: %v\n", sm.IsValid(sessionID))
132}

Database Mocking

In-memory database mocks simulate data persistence without real databases.

Comprehensive Database Mock

  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "sync"
  7)
  8
  9type Database interface {
 10    Create(user *User) (int, error)
 11    GetByID(id int) (*User, error)
 12    GetByEmail(email string) (*User, error)
 13    Update(user *User) error
 14    Delete(id int) error
 15    List() ([]*User, error)
 16}
 17
 18type User struct {
 19    ID    int
 20    Name  string
 21    Email string
 22    Age   int
 23}
 24
 25type MockDatabase struct {
 26    users  map[int]*User
 27    nextID int
 28    errors map[string]error
 29    mu     sync.RWMutex
 30}
 31
 32func NewMockDatabase() *MockDatabase {
 33    return &MockDatabase{
 34        users:  make(map[int]*User),
 35        nextID: 1,
 36        errors: make(map[string]error),
 37    }
 38}
 39
 40func (db *MockDatabase) SetError(operation string, err error) {
 41    db.mu.Lock()
 42    defer db.mu.Unlock()
 43    db.errors[operation] = err
 44}
 45
 46func (db *MockDatabase) ClearErrors() {
 47    db.mu.Lock()
 48    defer db.mu.Unlock()
 49    db.errors = make(map[string]error)
 50}
 51
 52func (db *MockDatabase) Create(user *User) (int, error) {
 53    db.mu.Lock()
 54    defer db.mu.Unlock()
 55
 56    if err := db.errors["create"]; err != nil {
 57        return 0, err
 58    }
 59
 60    if user.Email == "" {
 61        return 0, fmt.Errorf("email is required")
 62    }
 63
 64    // Check for duplicate email
 65    for _, existing := range db.users {
 66        if existing.Email == user.Email {
 67            return 0, fmt.Errorf("email already exists")
 68        }
 69    }
 70
 71    id := db.nextID
 72    user.ID = id
 73    db.users[id] = &User{
 74        ID:    user.ID,
 75        Name:  user.Name,
 76        Email: user.Email,
 77        Age:   user.Age,
 78    }
 79    db.nextID++
 80
 81    return id, nil
 82}
 83
 84func (db *MockDatabase) GetByID(id int) (*User, error) {
 85    db.mu.RLock()
 86    defer db.mu.RUnlock()
 87
 88    if err := db.errors["get"]; err != nil {
 89        return nil, err
 90    }
 91
 92    user, exists := db.users[id]
 93    if !exists {
 94        return nil, fmt.Errorf("user not found")
 95    }
 96
 97    return &User{
 98        ID:    user.ID,
 99        Name:  user.Name,
100        Email: user.Email,
101        Age:   user.Age,
102    }, nil
103}
104
105func (db *MockDatabase) GetByEmail(email string) (*User, error) {
106    db.mu.RLock()
107    defer db.mu.RUnlock()
108
109    if err := db.errors["get"]; err != nil {
110        return nil, err
111    }
112
113    for _, user := range db.users {
114        if user.Email == email {
115            return &User{
116                ID:    user.ID,
117                Name:  user.Name,
118                Email: user.Email,
119                Age:   user.Age,
120            }, nil
121        }
122    }
123
124    return nil, fmt.Errorf("user not found")
125}
126
127func (db *MockDatabase) Update(user *User) error {
128    db.mu.Lock()
129    defer db.mu.Unlock()
130
131    if err := db.errors["update"]; err != nil {
132        return err
133    }
134
135    existing, exists := db.users[user.ID]
136    if !exists {
137        return fmt.Errorf("user not found")
138    }
139
140    // Check for duplicate email
141    for id, u := range db.users {
142        if id != user.ID && u.Email == user.Email {
143            return fmt.Errorf("email already exists")
144        }
145    }
146
147    existing.Name = user.Name
148    existing.Email = user.Email
149    existing.Age = user.Age
150
151    return nil
152}
153
154func (db *MockDatabase) Delete(id int) error {
155    db.mu.Lock()
156    defer db.mu.Unlock()
157
158    if err := db.errors["delete"]; err != nil {
159        return err
160    }
161
162    if _, exists := db.users[id]; !exists {
163        return fmt.Errorf("user not found")
164    }
165
166    delete(db.users, id)
167    return nil
168}
169
170func (db *MockDatabase) List() ([]*User, error) {
171    db.mu.RLock()
172    defer db.mu.RUnlock()
173
174    if err := db.errors["list"]; err != nil {
175        return nil, err
176    }
177
178    users := make([]*User, 0, len(db.users))
179    for _, user := range db.users {
180        users = append(users, &User{
181            ID:    user.ID,
182            Name:  user.Name,
183            Email: user.Email,
184            Age:   user.Age,
185        })
186    }
187
188    return users, nil
189}
190
191func main() {
192    db := NewMockDatabase()
193
194    // Create users
195    id1, _ := db.Create(&User{Name: "Alice", Email: "alice@example.com", Age: 30})
196    id2, _ := db.Create(&User{Name: "Bob", Email: "bob@example.com", Age: 25})
197
198    fmt.Printf("Created users: %d, %d\n", id1, id2)
199
200    // Get user
201    user, _ := db.GetByID(id1)
202    fmt.Printf("Retrieved user: %s (%s)\n", user.Name, user.Email)
203
204    // Update user
205    user.Age = 31
206    db.Update(user)
207
208    // List all users
209    users, _ := db.List()
210    fmt.Printf("Total users: %d\n", len(users))
211
212    // Test error scenario
213    db.SetError("create", fmt.Errorf("database error"))
214    _, err := db.Create(&User{Name: "Charlie", Email: "charlie@example.com"})
215    fmt.Printf("Expected error: %v\n", err)
216}

Contract Testing

Contract testing ensures reliable service communication in distributed systems by defining and verifying API interactions between services.

Consumer-Driven Contracts

 1// run
 2package main
 3
 4import (
 5    "encoding/json"
 6    "fmt"
 7)
 8
 9type Contract struct {
10    Consumer     string        `json:"consumer"`
11    Provider     string        `json:"provider"`
12    Version      string        `json:"version"`
13    Interactions []Interaction `json:"interactions"`
14}
15
16type Interaction struct {
17    Description string   `json:"description"`
18    Request     Request  `json:"request"`
19    Response    Response `json:"response"`
20}
21
22type Request struct {
23    Method  string                 `json:"method"`
24    Path    string                 `json:"path"`
25    Headers map[string]string      `json:"headers,omitempty"`
26    Body    map[string]interface{} `json:"body,omitempty"`
27}
28
29type Response struct {
30    Status  int                    `json:"status"`
31    Headers map[string]string      `json:"headers,omitempty"`
32    Body    map[string]interface{} `json:"body"`
33}
34
35func main() {
36    contract := Contract{
37        Consumer: "web-frontend",
38        Provider: "user-service",
39        Version:  "1.0.0",
40        Interactions: []Interaction{
41            {
42                Description: "get user by ID",
43                Request: Request{
44                    Method: "GET",
45                    Path:   "/api/users/123",
46                    Headers: map[string]string{
47                        "Accept": "application/json",
48                    },
49                },
50                Response: Response{
51                    Status: 200,
52                    Headers: map[string]string{
53                        "Content-Type": "application/json",
54                    },
55                    Body: map[string]interface{}{
56                        "id":    123,
57                        "name":  "Alice",
58                        "email": "alice@example.com",
59                    },
60                },
61            },
62            {
63                Description: "create user",
64                Request: Request{
65                    Method: "POST",
66                    Path:   "/api/users",
67                    Headers: map[string]string{
68                        "Content-Type": "application/json",
69                    },
70                    Body: map[string]interface{}{
71                        "name":  "Bob",
72                        "email": "bob@example.com",
73                    },
74                },
75                Response: Response{
76                    Status: 201,
77                    Headers: map[string]string{
78                        "Content-Type": "application/json",
79                    },
80                    Body: map[string]interface{}{
81                        "id":    124,
82                        "name":  "Bob",
83                        "email": "bob@example.com",
84                    },
85                },
86            },
87        },
88    }
89
90    data, _ := json.MarshalIndent(contract, "", "  ")
91    fmt.Println(string(data))
92}

Contract Verification Framework

  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7    "io"
  8    "net/http"
  9    "net/http/httptest"
 10    "strings"
 11)
 12
 13type ContractVerifier struct {
 14    contract Contract
 15    baseURL  string
 16}
 17
 18func NewContractVerifier(contract Contract, baseURL string) *ContractVerifier {
 19    return &ContractVerifier{
 20        contract: contract,
 21        baseURL:  baseURL,
 22    }
 23}
 24
 25func (v *ContractVerifier) VerifyAll() []error {
 26    var errors []error
 27
 28    for _, interaction := range v.contract.Interactions {
 29        if err := v.verifyInteraction(interaction); err != nil {
 30            errors = append(errors, fmt.Errorf("%s: %w", interaction.Description, err))
 31        }
 32    }
 33
 34    return errors
 35}
 36
 37func (v *ContractVerifier) verifyInteraction(interaction Interaction) error {
 38    url := v.baseURL + interaction.Request.Path
 39    var body io.Reader
 40
 41    if interaction.Request.Body != nil {
 42        bodyBytes, _ := json.Marshal(interaction.Request.Body)
 43        body = strings.NewReader(string(bodyBytes))
 44    }
 45
 46    req, err := http.NewRequest(interaction.Request.Method, url, body)
 47    if err != nil {
 48        return err
 49    }
 50
 51    // Set headers
 52    for key, value := range interaction.Request.Headers {
 53        req.Header.Set(key, value)
 54    }
 55
 56    // Make request
 57    client := &http.Client{}
 58    resp, err := client.Do(req)
 59    if err != nil {
 60        return err
 61    }
 62    defer resp.Body.Close()
 63
 64    // Verify status code
 65    if resp.StatusCode != interaction.Response.Status {
 66        return fmt.Errorf("status code mismatch: expected %d, got %d",
 67            interaction.Response.Status, resp.StatusCode)
 68    }
 69
 70    // Verify headers
 71    for key, expectedValue := range interaction.Response.Headers {
 72        if actualValue := resp.Header.Get(key); actualValue != expectedValue {
 73            return fmt.Errorf("header %s mismatch: expected %s, got %s",
 74                key, expectedValue, actualValue)
 75        }
 76    }
 77
 78    // Verify body
 79    var responseBody map[string]interface{}
 80    if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil {
 81        return err
 82    }
 83
 84    // Check all expected fields exist
 85    for key, expectedValue := range interaction.Response.Body {
 86        actualValue, exists := responseBody[key]
 87        if !exists {
 88            return fmt.Errorf("missing field: %s", key)
 89        }
 90
 91        // Type check
 92        if fmt.Sprintf("%T", actualValue) != fmt.Sprintf("%T", expectedValue) {
 93            return fmt.Errorf("field %s type mismatch", key)
 94        }
 95    }
 96
 97    return nil
 98}
 99
100type Contract struct {
101    Consumer     string
102    Provider     string
103    Version      string
104    Interactions []Interaction
105}
106
107type Interaction struct {
108    Description string
109    Request     Request
110    Response    Response
111}
112
113type Request struct {
114    Method  string
115    Path    string
116    Headers map[string]string
117    Body    map[string]interface{}
118}
119
120type Response struct {
121    Status  int
122    Headers map[string]string
123    Body    map[string]interface{}
124}
125
126func main() {
127    // Create test server
128    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129        if r.URL.Path == "/api/users/123" && r.Method == "GET" {
130            w.Header().Set("Content-Type", "application/json")
131            json.NewEncoder(w).Encode(map[string]interface{}{
132                "id":    123,
133                "name":  "Alice",
134                "email": "alice@example.com",
135            })
136        } else {
137            w.WriteHeader(http.StatusNotFound)
138        }
139    }))
140    defer server.Close()
141
142    // Define contract
143    contract := Contract{
144        Consumer: "web-frontend",
145        Provider: "user-service",
146        Interactions: []Interaction{
147            {
148                Description: "get user by ID",
149                Request: Request{
150                    Method: "GET",
151                    Path:   "/api/users/123",
152                    Headers: map[string]string{
153                        "Accept": "application/json",
154                    },
155                },
156                Response: Response{
157                    Status: 200,
158                    Headers: map[string]string{
159                        "Content-Type": "application/json",
160                    },
161                    Body: map[string]interface{}{
162                        "id":    float64(123), // JSON numbers are float64
163                        "name":  "Alice",
164                        "email": "alice@example.com",
165                    },
166                },
167            },
168        },
169    }
170
171    // Verify contract
172    verifier := NewContractVerifier(contract, server.URL)
173    errors := verifier.VerifyAll()
174
175    if len(errors) == 0 {
176        fmt.Println("All contract verifications passed!")
177    } else {
178        fmt.Println("Contract verification failures:")
179        for _, err := range errors {
180            fmt.Printf("  - %v\n", err)
181        }
182    }
183}

Schema Validation

JSON schema validation enforces data contract compliance:

  1// run
  2package main
  3
  4import (
  5    "encoding/json"
  6    "fmt"
  7)
  8
  9type SchemaValidator struct {
 10    schema map[string]interface{}
 11}
 12
 13func NewSchemaValidator(schema map[string]interface{}) *SchemaValidator {
 14    return &SchemaValidator{schema: schema}
 15}
 16
 17func (v *SchemaValidator) Validate(data map[string]interface{}) []string {
 18    var errors []string
 19
 20    // Check required fields
 21    if required, ok := v.schema["required"].([]interface{}); ok {
 22        for _, field := range required {
 23            fieldName := field.(string)
 24            if _, exists := data[fieldName]; !exists {
 25                errors = append(errors, fmt.Sprintf("missing required field: %s", fieldName))
 26            }
 27        }
 28    }
 29
 30    // Check field types
 31    if properties, ok := v.schema["properties"].(map[string]interface{}); ok {
 32        for fieldName, fieldSchema := range properties {
 33            if value, exists := data[fieldName]; exists {
 34                schemaMap := fieldSchema.(map[string]interface{})
 35                expectedType := schemaMap["type"].(string)
 36
 37                if err := v.validateType(fieldName, value, expectedType); err != "" {
 38                    errors = append(errors, err)
 39                }
 40            }
 41        }
 42    }
 43
 44    return errors
 45}
 46
 47func (v *SchemaValidator) validateType(fieldName string, value interface{}, expectedType string) string {
 48    switch expectedType {
 49    case "string":
 50        if _, ok := value.(string); !ok {
 51            return fmt.Sprintf("field %s: expected string, got %T", fieldName, value)
 52        }
 53    case "number":
 54        if _, ok := value.(float64); !ok {
 55            return fmt.Sprintf("field %s: expected number, got %T", fieldName, value)
 56        }
 57    case "integer":
 58        if num, ok := value.(float64); !ok || num != float64(int(num)) {
 59            return fmt.Sprintf("field %s: expected integer, got %T", fieldName, value)
 60        }
 61    case "boolean":
 62        if _, ok := value.(bool); !ok {
 63            return fmt.Sprintf("field %s: expected boolean, got %T", fieldName, value)
 64        }
 65    }
 66    return ""
 67}
 68
 69func main() {
 70    // Define schema
 71    schema := map[string]interface{}{
 72        "type": "object",
 73        "required": []interface{}{"id", "name", "email"},
 74        "properties": map[string]interface{}{
 75            "id":    map[string]interface{}{"type": "integer"},
 76            "name":  map[string]interface{}{"type": "string"},
 77            "email": map[string]interface{}{"type": "string"},
 78            "age":   map[string]interface{}{"type": "integer"},
 79        },
 80    }
 81
 82    validator := NewSchemaValidator(schema)
 83
 84    // Valid data
 85    validData := map[string]interface{}{
 86        "id":    float64(1), // JSON numbers are float64
 87        "name":  "Alice",
 88        "email": "alice@example.com",
 89        "age":   float64(30),
 90    }
 91
 92    errors := validator.Validate(validData)
 93    fmt.Printf("Valid data errors: %v\n", errors)
 94
 95    // Invalid data (missing email)
 96    invalidData := map[string]interface{}{
 97        "id":   float64(1),
 98        "name": "Bob",
 99    }
100
101    errors = validator.Validate(invalidData)
102    fmt.Printf("Invalid data errors: %v\n", errors)
103
104    // Type mismatch
105    typeMismatch := map[string]interface{}{
106        "id":    "should-be-number",
107        "name":  "Charlie",
108        "email": "charlie@example.com",
109    }
110
111    errors = validator.Validate(typeMismatch)
112    fmt.Printf("Type mismatch errors: %v\n", errors)
113}

Best Practices

Mock Design Principles

  1. Keep mocks simple - Only implement what the test needs
  2. Use interfaces consistently - Don't mock concrete types
  3. Make assertions explicit - Verify behavior, not just state
  4. Avoid over-mocking - Mock only external dependencies
  5. Test both success and failure paths

Contract Testing Guidelines

  1. Define contracts early - Before implementation
  2. Automate verification - Run in CI/CD pipeline
  3. Version contracts - Maintain backward compatibility
  4. Document contracts - Serve as living documentation

Testing Anti-Patterns to Avoid

Anti-Pattern 1: Over-Mocking

 1// ❌ BAD: Mocking everything
 2func TestBusinessLogic_OverMocked(t *testing.T) {
 3    mockValidator := NewMockValidator()
 4    mockParser := NewMockParser()
 5    mockFormatter := NewMockFormatter()
 6    mockLogger := NewMockLogger()
 7    mockCache := NewMockCache()
 8    // Too many mocks - hard to maintain
 9}
10
11// ✅ GOOD: Mock only external dependencies
12func TestBusinessLogic_Minimal(t *testing.T) {
13    mockDB := NewMockDatabase()
14    mockAPI := NewMockAPIClient()
15    // Only mock what crosses boundaries
16}

Anti-Pattern 2: Testing Implementation Details

 1// ❌ BAD: Testing internal methods
 2func TestInternalImplementation(t *testing.T) {
 3    service := NewService()
 4    // Testing private method behavior
 5    result := service.internalHelper()
 6    // Brittle - breaks when refactoring
 7}
 8
 9// ✅ GOOD: Testing public API
10func TestPublicBehavior(t *testing.T) {
11    service := NewService()
12    result := service.PublicMethod()
13    // Tests observable behavior
14}

Practice Exercises

Exercise 1: Mock HTTP Client

Learning Objectives: Master interface-based mocking for HTTP clients to create isolated unit tests that don't depend on external services.

Real-World Context: Mocking HTTP clients is essential for testing services that interact with external APIs, ensuring fast, reliable tests that work offline and don't incur costs from third-party service calls.

Difficulty: ⭐⭐⭐ Intermediate
Time Estimate: 25-30 minutes

Create a mock HTTP client for testing a weather API wrapper. Implement an interface that abstracts HTTP calls, create a mock implementation that returns predictable data, and write tests that verify both successful responses and error handling scenarios.

Solution
 1package weather
 2
 3import (
 4    "fmt"
 5    "testing"
 6)
 7
 8type WeatherClient interface {
 9    GetWeather(city string) (*WeatherData, error)
10}
11
12type WeatherData struct {
13    City        string  `json:"city"`
14    Temperature float64 `json:"temperature"`
15    Condition   string  `json:"condition"`
16}
17
18type MockWeatherClient struct {
19    weatherData map[string]*WeatherData
20    errors      map[string]error
21}
22
23func NewMockWeatherClient() *MockWeatherClient {
24    return &MockWeatherClient{
25        weatherData: make(map[string]*WeatherData),
26        errors:      make(map[string]error),
27    }
28}
29
30func (m *MockWeatherClient) SetWeather(city string, data *WeatherData) {
31    m.weatherData[city] = data
32}
33
34func (m *MockWeatherClient) SetError(city string, err error) {
35    m.errors[city] = err
36}
37
38func (m *MockWeatherClient) GetWeather(city string) (*WeatherData, error) {
39    if err, exists := m.errors[city]; exists {
40        return nil, err
41    }
42    if data, exists := m.weatherData[city]; exists {
43        return data, nil
44    }
45    return nil, fmt.Errorf("no weather data for city: %s", city)
46}
47
48type WeatherService struct {
49    client WeatherClient
50}
51
52func NewWeatherService(client WeatherClient) *WeatherService {
53    return &WeatherService{client: client}
54}
55
56func (s *WeatherService) GetWeather(city string) (*WeatherData, error) {
57    return s.client.GetWeather(city)
58}
59
60func TestWeatherService(t *testing.T) {
61    mockClient := NewMockWeatherClient()
62    mockClient.SetWeather("London", &WeatherData{
63        City:        "London",
64        Temperature: 15.5,
65        Condition:   "Cloudy",
66    })
67
68    service := NewWeatherService(mockClient)
69    weather, err := service.GetWeather("London")
70
71    if err != nil {
72        t.Errorf("unexpected error: %v", err)
73    }
74
75    if weather.City != "London" {
76        t.Errorf("expected London, got %s", weather.City)
77    }
78
79    if weather.Temperature != 15.5 {
80        t.Errorf("expected 15.5, got %f", weather.Temperature)
81    }
82}
83
84func TestWeatherService_Error(t *testing.T) {
85    mockClient := NewMockWeatherClient()
86    mockClient.SetError("InvalidCity", fmt.Errorf("city not found"))
87
88    service := NewWeatherService(mockClient)
89    _, err := service.GetWeather("InvalidCity")
90
91    if err == nil {
92        t.Error("expected error, got nil")
93    }
94}

Exercise 2: Contract Test for API

Learning Objectives: Implement contract testing to verify API compliance and ensure reliable service communication in distributed systems.

Real-World Context: Contract testing is critical in microservices architecture to catch breaking changes early, ensure backward compatibility, and maintain reliable communication between services deployed independently.

Difficulty: ⭐⭐⭐⭐ Advanced
Time Estimate: 30-35 minutes

Write contract tests for a user management API. Define and verify API contracts that ensure both request/response formats and required fields are maintained across service versions, preventing integration issues in production.

Solution
 1package contract
 2
 3import (
 4    "encoding/json"
 5    "net/http"
 6    "net/http/httptest"
 7    "reflect"
 8    "testing"
 9)
10
11type User struct {
12    ID    int    `json:"id"`
13    Name  string `json:"name"`
14    Email string `json:"email"`
15}
16
17type UserAPIClient struct {
18    baseURL string
19}
20
21func NewUserAPIClient(baseURL string) *UserAPIClient {
22    return &UserAPIClient{baseURL: baseURL}
23}
24
25func (c *UserAPIClient) GetUser(id int) (*User, error) {
26    // Implementation would make HTTP request
27    return nil, nil
28}
29
30func (c *UserAPIClient) CreateUser(user *User) (*User, error) {
31    // Implementation would make HTTP request
32    return nil, nil
33}
34
35func TestUserAPIContract(t *testing.T) {
36    server := setupTestUserServer(t)
37    defer server.Close()
38
39    client := NewUserAPIClient(server.URL)
40
41    t.Run("get user contract", func(t *testing.T) {
42        resp, err := http.Get(server.URL + "/users/1")
43        if err != nil {
44            t.Fatalf("request failed: %v", err)
45        }
46        defer resp.Body.Close()
47
48        if resp.StatusCode != http.StatusOK {
49            t.Errorf("expected status 200, got %d", resp.StatusCode)
50        }
51
52        var user User
53        if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
54            t.Fatalf("decode failed: %v", err)
55        }
56
57        // Verify contract fields
58        requiredFields := []string{"ID", "Name", "Email"}
59        userValue := reflect.ValueOf(user)
60
61        for _, field := range requiredFields {
62            fieldValue := userValue.FieldByName(field)
63            if fieldValue.IsZero() {
64                t.Errorf("contract violation: %s should be non-zero", field)
65            }
66        }
67    })
68
69    t.Run("create user contract", func(t *testing.T) {
70        // Test create user contract
71        // Implementation would verify response structure
72    })
73}
74
75func setupTestUserServer(t *testing.T) *httptest.Server {
76    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
77        if r.URL.Path == "/users/1" && r.Method == "GET" {
78            user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
79            w.Header().Set("Content-Type", "application/json")
80            json.NewEncoder(w).Encode(user)
81        } else {
82            w.WriteHeader(http.StatusNotFound)
83        }
84    }))
85}

Exercise 3: Time Mocking Challenge

Learning Objectives: Implement time mocking to test time-dependent functionality without actual delays, enabling fast and deterministic tests.

Real-World Context: Time mocking is crucial for testing caching, session management, rate limiting, and scheduled tasks without waiting for real time to pass, significantly speeding up test suites and making them reliable.

Difficulty: ⭐⭐⭐ Intermediate
Time Estimate: 25-30 minutes

Test a caching service that uses time-based expiration. Implement a clock interface that allows controlling time in tests, enabling verification of expiration logic and time-dependent behavior without actual delays.

Solution
 1package cache
 2
 3import (
 4    "testing"
 5    "time"
 6)
 7
 8type Clock interface {
 9    Now() time.Time
10}
11
12type MockClock struct {
13    currentTime time.Time
14}
15
16func NewMockClock(start time.Time) *MockClock {
17    return &MockClock{currentTime: start}
18}
19
20func (c *MockClock) Now() time.Time {
21    return c.currentTime
22}
23
24func (c *MockClock) Advance(d time.Duration) {
25    c.currentTime = c.currentTime.Add(d)
26}
27
28type Cache struct {
29    clock Clock
30    items map[string]*CacheItem
31}
32
33type CacheItem struct {
34    Value     interface{}
35    ExpiresAt time.Time
36}
37
38func NewCache(clock Clock) *Cache {
39    return &Cache{
40        clock: clock,
41        items: make(map[string]*CacheItem),
42    }
43}
44
45func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
46    c.items[key] = &CacheItem{
47        Value:     value,
48        ExpiresAt: c.clock.Now().Add(ttl),
49    }
50}
51
52func (c *Cache) Get(key string) (interface{}, bool) {
53    item, exists := c.items[key]
54    if !exists || c.clock.Now().After(item.ExpiresAt) {
55        return nil, false
56    }
57    return item.Value, true
58}
59
60func TestCache_TimeMocking(t *testing.T) {
61    mockClock := NewMockClock(time.Now())
62    cache := NewCache(mockClock)
63
64    cache.Set("key1", "value1", time.Hour)
65
66    if _, exists := cache.Get("key1"); !exists {
67        t.Error("item should be available immediately")
68    }
69
70    mockClock.Advance(75 * time.Minute)
71    if _, exists := cache.Get("key1"); exists {
72        t.Error("item should be expired after 75 minutes")
73    }
74}

Exercise 4: Database Mocking with State

Learning Objectives: Create sophisticated database mocks that maintain state and support complex query operations for testing data access layers.

Real-World Context: Stateful database mocks enable testing of complex business logic, data transformations, and error scenarios without the overhead and brittleness of real database connections in test environments.

Difficulty: ⭐⭐⭐⭐ Advanced
Time Estimate: 35-40 minutes

Implement a stateful mock database that supports CRUD operations, queries, and transaction-like behavior. Create a repository pattern that uses the mock database to test complex business logic including data validation, relationships, and error handling.

Solution
  1package repository
  2
  3import (
  4    "errors"
  5    "fmt"
  6    "strings"
  7    "sync"
  8    "testing"
  9    "time"
 10)
 11
 12type User struct {
 13    ID        int
 14    Name      string
 15    Email     string
 16    Age       int
 17    Active    bool
 18    CreatedAt time.Time
 19    UpdatedAt time.Time
 20}
 21
 22type Database interface {
 23    Create(user *User) (int, error)
 24    GetByID(id int) (*User, error)
 25    GetByEmail(email string) (*User, error)
 26    Update(user *User) error
 27    Delete(id int) error
 28    List(filters UserFilters) ([]*User, error)
 29}
 30
 31type UserFilters struct {
 32    NameContains string
 33    MinAge       int
 34    MaxAge       int
 35    Active       *bool
 36}
 37
 38type MockDatabase struct {
 39    users    map[int]*User
 40    nextID   int
 41    mu       sync.RWMutex
 42    failures map[string]bool
 43}
 44
 45func NewMockDatabase() *MockDatabase {
 46    return &MockDatabase{
 47        users:    make(map[int]*User),
 48        nextID:   1,
 49        failures: make(map[string]bool),
 50    }
 51}
 52
 53func (db *MockDatabase) SetFailure(operation string, fail bool) {
 54    db.mu.Lock()
 55    defer db.mu.Unlock()
 56    db.failures[operation] = fail
 57}
 58
 59func (db *MockDatabase) Create(user *User) (int, error) {
 60    db.mu.Lock()
 61    defer db.mu.Unlock()
 62
 63    if db.failures["create"] {
 64        return 0, errors.New("database create failed")
 65    }
 66
 67    if user.Email == "" {
 68        return 0, errors.New("email is required")
 69    }
 70
 71    for _, existing := range db.users {
 72        if existing.Email == user.Email {
 73            return 0, errors.New("email already exists")
 74        }
 75    }
 76
 77    user.ID = db.nextID
 78    user.CreatedAt = time.Now()
 79    user.UpdatedAt = time.Now()
 80
 81    db.users[user.ID] = &User{
 82        ID:        user.ID,
 83        Name:      user.Name,
 84        Email:     user.Email,
 85        Age:       user.Age,
 86        Active:    user.Active,
 87        CreatedAt: user.CreatedAt,
 88        UpdatedAt: user.UpdatedAt,
 89    }
 90
 91    db.nextID++
 92    return user.ID, nil
 93}
 94
 95func (db *MockDatabase) GetByID(id int) (*User, error) {
 96    db.mu.RLock()
 97    defer db.mu.RUnlock()
 98
 99    if db.failures["read"] {
100        return nil, errors.New("database read failed")
101    }
102
103    user, exists := db.users[id]
104    if !exists {
105        return nil, fmt.Errorf("user with id %d not found", id)
106    }
107
108    return &User{
109        ID:        user.ID,
110        Name:      user.Name,
111        Email:     user.Email,
112        Age:       user.Age,
113        Active:    user.Active,
114        CreatedAt: user.CreatedAt,
115        UpdatedAt: user.UpdatedAt,
116    }, nil
117}
118
119func (db *MockDatabase) GetByEmail(email string) (*User, error) {
120    db.mu.RLock()
121    defer db.mu.RUnlock()
122
123    if db.failures["read"] {
124        return nil, errors.New("database read failed")
125    }
126
127    for _, user := range db.users {
128        if user.Email == email {
129            return &User{
130                ID:        user.ID,
131                Name:      user.Name,
132                Email:     user.Email,
133                Age:       user.Age,
134                Active:    user.Active,
135                CreatedAt: user.CreatedAt,
136                UpdatedAt: user.UpdatedAt,
137            }, nil
138        }
139    }
140
141    return nil, fmt.Errorf("user with email %s not found", email)
142}
143
144func (db *MockDatabase) Update(user *User) error {
145    db.mu.Lock()
146    defer db.mu.Unlock()
147
148    if db.failures["update"] {
149        return errors.New("database update failed")
150    }
151
152    existing, exists := db.users[user.ID]
153    if !exists {
154        return fmt.Errorf("user with id %d not found", user.ID)
155    }
156
157    for id, existingUser := range db.users {
158        if id != user.ID && existingUser.Email == user.Email {
159            return errors.New("email already exists")
160        }
161    }
162
163    existing.Name = user.Name
164    existing.Email = user.Email
165    existing.Age = user.Age
166    existing.Active = user.Active
167    existing.UpdatedAt = time.Now()
168
169    return nil
170}
171
172func (db *MockDatabase) Delete(id int) error {
173    db.mu.Lock()
174    defer db.mu.Unlock()
175
176    if db.failures["delete"] {
177        return errors.New("database delete failed")
178    }
179
180    if _, exists := db.users[id]; !exists {
181        return fmt.Errorf("user with id %d not found", id)
182    }
183
184    delete(db.users, id)
185    return nil
186}
187
188func (db *MockDatabase) List(filters UserFilters) ([]*User, error) {
189    db.mu.RLock()
190    defer db.mu.RUnlock()
191
192    if db.failures["list"] {
193        return nil, errors.New("database list failed")
194    }
195
196    var results []*User
197
198    for _, user := range db.users {
199        if filters.NameContains != "" && !strings.Contains(strings.ToLower(user.Name), strings.ToLower(filters.NameContains)) {
200            continue
201        }
202
203        if filters.MinAge > 0 && user.Age < filters.MinAge {
204            continue
205        }
206
207        if filters.MaxAge > 0 && user.Age > filters.MaxAge {
208            continue
209        }
210
211        if filters.Active != nil && *filters.Active != user.Active {
212            continue
213        }
214
215        results = append(results, &User{
216            ID:        user.ID,
217            Name:      user.Name,
218            Email:     user.Email,
219            Age:       user.Age,
220            Active:    user.Active,
221            CreatedAt: user.CreatedAt,
222            UpdatedAt: user.UpdatedAt,
223        })
224    }
225
226    return results, nil
227}
228
229type UserRepository struct {
230    db Database
231}
232
233func NewUserRepository(db Database) *UserRepository {
234    return &UserRepository{db: db}
235}
236
237func (r *UserRepository) CreateUser(user *User) error {
238    if user.Name == "" {
239        return errors.New("name is required")
240    }
241
242    if user.Age < 0 || user.Age > 150 {
243        return errors.New("invalid age")
244    }
245
246    id, err := r.db.Create(user)
247    if err != nil {
248        return fmt.Errorf("failed to create user: %w", err)
249    }
250
251    user.ID = id
252    return nil
253}
254
255func (r *UserRepository) GetUser(id int) (*User, error) {
256    user, err := r.db.GetByID(id)
257    if err != nil {
258        return nil, fmt.Errorf("failed to get user: %w", err)
259    }
260
261    if !user.Active {
262        return nil, errors.New("user is inactive")
263    }
264
265    return user, nil
266}
267
268func TestUserRepository_CreateUser(t *testing.T) {
269    mockDB := NewMockDatabase()
270    repo := NewUserRepository(mockDB)
271
272    tests := []struct {
273        name        string
274        user        *User
275        expectError bool
276        errorMsg    string
277    }{
278        {
279            name: "valid user",
280            user: &User{
281                Name:   "John Doe",
282                Email:  "john@example.com",
283                Age:    30,
284                Active: true,
285            },
286            expectError: false,
287        },
288        {
289            name: "missing name",
290            user: &User{
291                Email:  "john@example.com",
292                Age:    30,
293                Active: true,
294            },
295            expectError: true,
296            errorMsg:    "name is required",
297        },
298        {
299            name: "invalid age",
300            user: &User{
301                Name:   "John Doe",
302                Email:  "john@example.com",
303                Age:    -5,
304                Active: true,
305            },
306            expectError: true,
307            errorMsg:    "invalid age",
308        },
309    }
310
311    for _, tt := range tests {
312        t.Run(tt.name, func(t *testing.T) {
313            user := &User{
314                Name:   tt.user.Name,
315                Email:  tt.user.Email,
316                Age:    tt.user.Age,
317                Active: tt.user.Active,
318            }
319
320            err := repo.CreateUser(user)
321
322            if tt.expectError && err == nil {
323                t.Errorf("expected error, got nil")
324            }
325
326            if err != nil && tt.expectError {
327                if !strings.Contains(err.Error(), tt.errorMsg) {
328                    t.Errorf("expected error containing '%s', got '%v'", tt.errorMsg, err)
329                }
330            }
331        })
332    }
333}

Exercise 5: Advanced Contract Testing with Schema Validation

Learning Objectives: Implement comprehensive contract testing with JSON schema validation to ensure API compatibility and data integrity across service boundaries.

Real-World Context: Contract testing with schema validation is essential in microservices ecosystems to prevent breaking changes, ensure data quality, and maintain reliable integrations between independently deployed services.

Difficulty: ⭐⭐⭐⭐⭐ Expert
Time Estimate: 40-45 minutes

Create a comprehensive contract testing framework that validates API responses against JSON schemas, tests multiple contract scenarios, and verifies backward compatibility. Implement both provider and consumer tests to ensure service communication remains reliable during development and deployment cycles.

Solution
  1package contract
  2
  3import (
  4    "encoding/json"
  5    "fmt"
  6    "net/http"
  7    "net/http/httptest"
  8    "testing"
  9)
 10
 11type APIContract struct {
 12    Name      string
 13    Version   string
 14    Endpoints []EndpointContract
 15}
 16
 17type EndpointContract struct {
 18    Path        string
 19    Method      string
 20    Description string
 21    Request     RequestContract
 22    Response    ResponseContract
 23}
 24
 25type RequestContract struct {
 26    Headers map[string]string
 27    Body    map[string]interface{}
 28}
 29
 30type ResponseContract struct {
 31    Status int
 32    Body   map[string]interface{}
 33}
 34
 35type ContractTestSuite struct {
 36    contract APIContract
 37    baseURL  string
 38}
 39
 40func NewContractTestSuite(contract APIContract, baseURL string) *ContractTestSuite {
 41    return &ContractTestSuite{
 42        contract: contract,
 43        baseURL:  baseURL,
 44    }
 45}
 46
 47func (suite *ContractTestSuite) RunAllTests(t *testing.T) {
 48    for _, endpoint := range suite.contract.Endpoints {
 49        t.Run(fmt.Sprintf("%s %s", endpoint.Method, endpoint.Path), func(t *testing.T) {
 50            suite.testEndpoint(t, endpoint)
 51        })
 52    }
 53}
 54
 55func (suite *ContractTestSuite) testEndpoint(t *testing.T, endpoint EndpointContract) {
 56    url := suite.baseURL + endpoint.Path
 57
 58    req, err := http.NewRequest(endpoint.Method, url, nil)
 59    if err != nil {
 60        t.Fatalf("failed to create request: %v", err)
 61    }
 62
 63    for key, value := range endpoint.Request.Headers {
 64        req.Header.Set(key, value)
 65    }
 66
 67    client := &http.Client{}
 68    resp, err := client.Do(req)
 69    if err != nil {
 70        t.Fatalf("request failed: %v", err)
 71    }
 72    defer resp.Body.Close()
 73
 74    if resp.StatusCode != endpoint.Response.Status {
 75        t.Errorf("expected status %d, got %d", endpoint.Response.Status, resp.StatusCode)
 76    }
 77
 78    var responseBody map[string]interface{}
 79    if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil {
 80        t.Fatalf("failed to decode response: %v", err)
 81    }
 82
 83    suite.validateResponseSchema(t, endpoint, responseBody)
 84}
 85
 86func (suite *ContractTestSuite) validateResponseSchema(t *testing.T, endpoint EndpointContract, response map[string]interface{}) {
 87    for key, expectedValue := range endpoint.Response.Body {
 88        actualValue, exists := response[key]
 89        if !exists {
 90            t.Errorf("missing field: %s", key)
 91            continue
 92        }
 93
 94        if fmt.Sprintf("%T", actualValue) != fmt.Sprintf("%T", expectedValue) {
 95            t.Errorf("field %s type mismatch: expected %T, got %T", key, expectedValue, actualValue)
 96        }
 97    }
 98}
 99
100func TestUserAPIContract(t *testing.T) {
101    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102        if r.URL.Path == "/api/users/123" && r.Method == "GET" {
103            w.Header().Set("Content-Type", "application/json")
104            json.NewEncoder(w).Encode(map[string]interface{}{
105                "id":    float64(123),
106                "name":  "Alice Johnson",
107                "email": "alice@example.com",
108                "age":   float64(30),
109            })
110        } else {
111            w.WriteHeader(http.StatusNotFound)
112        }
113    }))
114    defer server.Close()
115
116    contract := APIContract{
117        Name:    "User API",
118        Version: "1.0.0",
119        Endpoints: []EndpointContract{
120            {
121                Path:        "/api/users/123",
122                Method:      "GET",
123                Description: "Get user by ID",
124                Response: ResponseContract{
125                    Status: http.StatusOK,
126                    Body: map[string]interface{}{
127                        "id":    float64(0),
128                        "name":  "",
129                        "email": "",
130                        "age":   float64(0),
131                    },
132                },
133            },
134        },
135    }
136
137    suite := NewContractTestSuite(contract, server.URL)
138    suite.RunAllTests(t)
139}

Exercise 6: Spy Pattern for Behavior Verification

Learning Objectives: Implement the spy pattern to record method calls and verify interaction patterns between components.

Real-World Context: Spy mocks are essential for verifying that components interact correctly, ensuring methods are called with the right parameters in the right order, crucial for testing event-driven systems and callbacks.

Difficulty: ⭐⭐⭐ Intermediate
Time Estimate: 25-30 minutes

Create a notification service spy that records all notification attempts and provides verification methods to assert specific behaviors occurred during test execution.

Solution
 1package notification
 2
 3import (
 4    "testing"
 5    "time"
 6)
 7
 8type NotificationService interface {
 9    SendEmail(to, subject, body string) error
10    SendSMS(phone, message string) error
11}
12
13type NotificationSpy struct {
14    EmailCalls []EmailCall
15    SMSCalls   []SMSCall
16}
17
18type EmailCall struct {
19    To        string
20    Subject   string
21    Body      string
22    Timestamp time.Time
23}
24
25type SMSCall struct {
26    Phone     string
27    Message   string
28    Timestamp time.Time
29}
30
31func NewNotificationSpy() *NotificationSpy {
32    return &NotificationSpy{
33        EmailCalls: make([]EmailCall, 0),
34        SMSCalls:   make([]SMSCall, 0),
35    }
36}
37
38func (s *NotificationSpy) SendEmail(to, subject, body string) error {
39    s.EmailCalls = append(s.EmailCalls, EmailCall{
40        To:        to,
41        Subject:   subject,
42        Body:      body,
43        Timestamp: time.Now(),
44    })
45    return nil
46}
47
48func (s *NotificationSpy) SendSMS(phone, message string) error {
49    s.SMSCalls = append(s.SMSCalls, SMSCall{
50        Phone:     phone,
51        Message:   message,
52        Timestamp: time.Now(),
53    })
54    return nil
55}
56
57func (s *NotificationSpy) EmailWasSentTo(email string) bool {
58    for _, call := range s.EmailCalls {
59        if call.To == email {
60            return true
61        }
62    }
63    return false
64}
65
66func (s *NotificationSpy) SMSWasSentTo(phone string) bool {
67    for _, call := range s.SMSCalls {
68        if call.Phone == phone {
69            return true
70        }
71    }
72    return false
73}
74
75func (s *NotificationSpy) TotalNotifications() int {
76    return len(s.EmailCalls) + len(s.SMSCalls)
77}
78
79func TestNotificationSpy(t *testing.T) {
80    spy := NewNotificationSpy()
81
82    spy.SendEmail("alice@example.com", "Welcome", "Hello Alice!")
83    spy.SendSMS("+1234567890", "Verification code: 123456")
84
85    if !spy.EmailWasSentTo("alice@example.com") {
86        t.Error("expected email to alice@example.com")
87    }
88
89    if !spy.SMSWasSentTo("+1234567890") {
90        t.Error("expected SMS to +1234567890")
91    }
92
93    if spy.TotalNotifications() != 2 {
94        t.Errorf("expected 2 notifications, got %d", spy.TotalNotifications())
95    }
96}

Summary

What You've Mastered

🎯 Advanced Testing Concepts:

  • Interface-based mocking enables isolated unit testing
  • Contract testing ensures reliable service communication
  • Time and database mocking eliminate dependencies

🔧 Practical Mocking Skills:

  • Stateful mocks maintain test data
  • Conditional mocks respond based on input
  • Spy pattern records interactions for verification

🏗️ Production Testing Patterns:

  • HTTP mocking with httptest provides controlled environments
  • Schema validation enforces data contracts
  • Contract verification prevents integration failures

Next Steps in Advanced Testing

🚀 Topics to Explore:

  1. Property-Based Testing: Generate test cases automatically
  2. Mutation Testing: Verify test suite effectiveness
  3. Chaos Engineering: Test system resilience
  4. Performance Testing: Load testing and stress testing

📚 Recommended Resources:

  • "Working Effectively with Legacy Code" by Michael Feathers
  • "Test-Driven Development: By Example" by Kent Beck
  • "xUnit Test Patterns" by Gerard Meszaros

Production Testing Checklist

  • Mock external dependencies: APIs, databases, file systems
  • Contract tests: Verify service integration
  • Time-based tests: Use clock interfaces
  • Error scenarios: Network failures, timeouts
  • Performance tests: Benchmark critical paths
  • Schema validation: Enforce data contracts

Master these advanced testing techniques to build robust, reliable distributed systems!