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
- Keep mocks simple - Only implement what the test needs
- Use interfaces consistently - Don't mock concrete types
- Make assertions explicit - Verify behavior, not just state
- Avoid over-mocking - Mock only external dependencies
- Test both success and failure paths
Contract Testing Guidelines
- Define contracts early - Before implementation
- Automate verification - Run in CI/CD pipeline
- Version contracts - Maintain backward compatibility
- 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:
- Property-Based Testing: Generate test cases automatically
- Mutation Testing: Verify test suite effectiveness
- Chaos Engineering: Test system resilience
- 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!