Go Testing and Benchmarking
Learning Objectives: By the end of this lesson, you will be able to:
- Write comprehensive unit tests using Go’s testing package
- Implement table-driven tests for thorough test coverage
- Create and analyze benchmarks for performance optimization
- Use testing tools like testify and generate mocks
- Measure and improve code coverage
- Profile applications to identify performance bottlenecks
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Testing is the safety net that allows developers to refactor fearlessly and deploy confidently. In Go, testing is built into the language itself, making it both powerful and approachable. Today we’ll master Go’s testing ecosystem and learn how to write tests that actually add value to your codebase.
Lesson Overview
- Duration: 2-3 days (6-8 hours)
- Prerequisites: Basic Go syntax, functions, structs, interfaces
- Materials Needed: Go development environment, testing tools
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Unit Testing Fundamentals - Writing effective unit tests
- Table-Driven Tests - Testing multiple scenarios efficiently
- Test Organization - Structuring tests for maintainability
- Mocking and Interfaces - Testing with dependencies
- Benchmarking - Measuring and optimizing performance
- Integration Testing - Testing complete workflows
- Profiling - Identifying performance bottlenecks
Success Criteria:
By the end of this session, you should be able to write a comprehensive test suite that gives you confidence in your code’s correctness and performance.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
Go’s Testing Philosophy
Go takes a minimalist approach to testing. The standard library’s testing package provides everything you need for unit tests, benchmarks, and examples without external dependencies.
Real-World Analogy: Think of testing like having a quality control process in manufacturing - you check each component individually (unit tests) and then test the assembled product (integration tests).
Why This Matters: Good tests serve as documentation, catch regressions, and enable confident refactoring. They’re an investment in code quality and developer productivity.
Test File Conventions
- Test files end with
_test.go - Test functions start with
Testand take*testing.T - Benchmark functions start with
Benchmarkand take*testing.B - Example functions start with
Example
Key Terminology
- Unit Test: Tests a single function or method in isolation
- Integration Test: Tests interaction between multiple components
- Table-Driven Test: Uses a table of test cases to test multiple scenarios
- Mock: Fake implementation used to isolate code under test
- Coverage: Percentage of code executed by tests
- Benchmark: Performance test that measures execution time
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Unit Testing
Unit testing in Go is straightforward and powerful. Let’s start with basic patterns and build up to more complex scenarios.
Basic Unit Testing Patterns
package calculator
import (
"errors"
"math"
)
// Calculator represents a basic calculator
type Calculator struct{}
// Add performs addition
func (c Calculator) Add(a, b float64) float64 {
return a + b
}
// Subtract performs subtraction
func (c Calculator) Subtract(a, b float64) float64 {
return a - b
}
// Multiply performs multiplication
func (c Calculator) Multiply(a, b float64) float64 {
return a * b
}
// Divide performs division with error handling
func (c Calculator) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Sqrt calculates square root
func (c Calculator) Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errors.New("cannot calculate square root of negative number")
}
return math.Sqrt(x), nil
}
// Power calculates a to the power of b
func (c Calculator) Power(a, b float64) float64 {
return math.Pow(a, b)
}
package calculator
import (
"math"
"testing"
)
func TestCalculator_Add(t *testing.T) {
calc := Calculator{}
result := calc.Add(2, 3)
expected := 5.0
if result != expected {
t.Errorf("Add(2, 3) = %v, want %v", result, expected)
}
}
func TestCalculator_Subtract(t *testing.T) {
calc := Calculator{}
result := calc.Subtract(5, 3)
expected := 2.0
if result != expected {
t.Errorf("Subtract(5, 3) = %v, want %v", result, expected)
}
}
func TestCalculator_Multiply(t *testing.T) {
calc := Calculator{}
result := calc.Multiply(4, 3)
expected := 12.0
if result != expected {
t.Errorf("Multiply(4, 3) = %v, want %v", result, expected)
}
}
func TestCalculator_Divide(t *testing.T) {
calc := Calculator{}
// Test successful division
result, err := calc.Divide(6, 2)
if err != nil {
t.Fatalf("Divide(6, 2) returned unexpected error: %v", err)
}
expected := 3.0
if result != expected {
t.Errorf("Divide(6, 2) = %v, want %v", result, expected)
}
}
func TestCalculator_Divide_DivisionByZero(t *testing.T) {
calc := Calculator{}
_, err := calc.Divide(5, 0)
if err == nil {
t.Error("Divide(5, 0) should return an error, got nil")
}
expectedError := "division by zero"
if err.Error() != expectedError {
t.Errorf("Divide(5, 0) error = %v, want %v", err.Error(), expectedError)
}
}
func TestCalculator_Sqrt(t *testing.T) {
calc := Calculator{}
// Test successful square root
result, err := calc.Sqrt(9)
if err != nil {
t.Fatalf("Sqrt(9) returned unexpected error: %v", err)
}
expected := 3.0
if result != expected {
t.Errorf("Sqrt(9) = %v, want %v", result, expected)
}
}
func TestCalculator_Sqrt_NegativeNumber(t *testing.T) {
calc := Calculator{}
_, err := calc.Sqrt(-4)
if err == nil {
t.Error("Sqrt(-4) should return an error, got nil")
}
expectedError := "cannot calculate square root of negative number"
if err.Error() != expectedError {
t.Errorf("Sqrt(-4) error = %v, want %v", err.Error(), expectedError)
}
}
func TestCalculator_Power(t *testing.T) {
calc := Calculator{}
result := calc.Power(2, 3)
expected := 8.0
if result != expected {
t.Errorf("Power(2, 3) = %v, want %v", result, expected)
}
}
Table-Driven Tests
Table-driven tests are a Go idiom for testing multiple scenarios efficiently:
func TestCalculator_Add_TableDriven(t *testing.T) {
calc := Calculator{}
tests := []struct {
name string
a, b float64
expected float64
}{
{
name: "positive numbers",
a: 2.5,
b: 3.7,
expected: 6.2,
},
{
name: "negative numbers",
a: -5.0,
b: -3.0,
expected: -8.0,
},
{
name: "mixed signs",
a: 10.0,
b: -4.0,
expected: 6.0,
},
{
name: "zero values",
a: 0.0,
b: 0.0,
expected: 0.0,
},
{
name: "large numbers",
a: 1000000.0,
b: 2000000.0,
expected: 3000000.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calc.Add(tt.a, tt.b)
if math.Abs(result-tt.expected) > 1e-9 {
t.Errorf("Add(%v, %v) = %v, want %v", tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestCalculator_Divide_TableDriven(t *testing.T) {
calc := Calculator{}
tests := []struct {
name string
a, b float64
expected float64
expectError bool
errorMsg string
}{
{
name: "normal division",
a: 10.0,
b: 2.0,
expected: 5.0,
expectError: false,
},
{
name: "division by zero",
a: 5.0,
b: 0.0,
expectError: true,
errorMsg: "division by zero",
},
{
name: "negative result",
a: -15.0,
b: 3.0,
expected: -5.0,
expectError: false,
},
{
name: "decimal result",
a: 7.0,
b: 3.0,
expected: 2.3333333333333335,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := calc.Divide(tt.a, tt.b)
if tt.expectError {
if err == nil {
t.Errorf("Divide(%v, %v) should return an error, got nil", tt.a, tt.b)
return
}
if err.Error() != tt.errorMsg {
t.Errorf("Divide(%v, %v) error = %v, want %v", tt.a, tt.b, err.Error(), tt.errorMsg)
}
return
}
if err != nil {
t.Errorf("Divide(%v, %v) returned unexpected error: %v", tt.a, tt.b, err)
return
}
if math.Abs(result-tt.expected) > 1e-9 {
t.Errorf("Divide(%v, %v) = %v, want %v", tt.a, tt.b, result, tt.expected)
}
})
}
}
Testing with Dependencies and Mocks
package userservice
import (
"errors"
"fmt"
"regexp"
"strings"
)
// User represents a user in the system
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"full_name"`
}
// UserRepository defines the interface for user data access
type UserRepository interface {
GetByID(id int) (*User, error)
GetByUsername(username string) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}
// EmailService defines the interface for sending emails
type EmailService interface {
SendWelcomeEmail(user *User) error
SendPasswordResetEmail(user *User, token string) error
}
// UserService handles user business logic
type UserService struct {
repo UserRepository
email EmailService
}
// NewUserService creates a new user service
func NewUserService(repo UserRepository, email EmailService) *UserService {
return &UserService{
repo: repo,
email: email,
}
}
// ValidateUser validates user data
func (us *UserService) ValidateUser(user *User) error {
if strings.TrimSpace(user.Username) == "" {
return errors.New("username cannot be empty")
}
if len(user.Username) < 3 {
return errors.New("username must be at least 3 characters")
}
if len(user.Username) > 20 {
return errors.New("username cannot exceed 20 characters")
}
// Email validation
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(user.Email) {
return errors.New("invalid email format")
}
if strings.TrimSpace(user.FullName) == "" {
return errors.New("full name cannot be empty")
}
return nil
}
// CreateUser creates a new user and sends welcome email
func (us *UserService) CreateUser(user *User) error {
// Validate user data
if err := us.ValidateUser(user); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Check if username already exists
existingUser, err := us.repo.GetByUsername(user.Username)
if err == nil && existingUser != nil {
return errors.New("username already exists")
}
// Create user
if err := us.repo.Create(user); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
// Send welcome email
if err := us.email.SendWelcomeEmail(user); err != nil {
// Log error but don't fail the operation
// In a real app, you might want to queue this for retry
fmt.Printf("Failed to send welcome email to %s: %v\n", user.Email, err)
}
return nil
}
// GetUser retrieves a user by ID
func (us *UserService) GetUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("invalid user ID")
}
user, err := us.repo.GetByID(id)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
// UpdateUser updates an existing user
func (us *UserService) UpdateUser(user *User) error {
if user.ID <= 0 {
return errors.New("invalid user ID")
}
// Validate user data
if err := us.ValidateUser(user); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Check if user exists
_, err := us.repo.GetByID(user.ID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
// Update user
if err := us.repo.Update(user); err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
return nil
}
Now let’s test this service with mocks:
package userservice
import (
"errors"
"testing"
)
// MockUserRepository implements UserRepository for testing
type MockUserRepository struct {
users map[int]*User
usersByName map[string]*User
nextID int
shouldFailGet bool
shouldFailCreate bool
shouldFailUpdate bool
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int]*User),
usersByName: make(map[string]*User),
nextID: 1,
}
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
if m.shouldFailGet {
return nil, errors.New("database error")
}
user, exists := m.users[id]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepository) GetByUsername(username string) (*User, error) {
if m.shouldFailGet {
return nil, errors.New("database error")
}
user, exists := m.usersByName[username]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepository) Create(user *User) error {
if m.shouldFailCreate {
return errors.New("database error")
}
user.ID = m.nextID
m.nextID++
m.users[user.ID] = user
m.usersByName[user.Username] = user
return nil
}
func (m *MockUserRepository) Update(user *User) error {
if m.shouldFailUpdate {
return errors.New("database error")
}
// Remove old username mapping
if existingUser, exists := m.users[user.ID]; exists {
delete(m.usersByName, existingUser.Username)
}
m.users[user.ID] = user
m.usersByName[user.Username] = user
return nil
}
func (m *MockUserRepository) Delete(id int) error {
if user, exists := m.users[id]; exists {
delete(m.users, id)
delete(m.usersByName, user.Username)
}
return nil
}
// MockEmailService implements EmailService for testing
type MockEmailService struct {
sentEmails []string
shouldFailWelcome bool
shouldFailReset bool
}
func NewMockEmailService() *MockEmailService {
return &MockEmailService{
sentEmails: make([]string, 0),
}
}
func (m *MockEmailService) SendWelcomeEmail(user *User) error {
if m.shouldFailWelcome {
return errors.New("email service error")
}
m.sentEmails = append(m.sentEmails, "welcome:"+user.Email)
return nil
}
func (m *MockEmailService) SendPasswordResetEmail(user *User, token string) error {
if m.shouldFailReset {
return errors.New("email service error")
}
m.sentEmails = append(m.sentEmails, "reset:"+user.Email+":"+token)
return nil
}
func (m *MockEmailService) GetSentEmails() []string {
return m.sentEmails
}
// Test cases using mocks
func TestUserService_ValidateUser(t *testing.T) {
service := NewUserService(nil, nil) // No dependencies needed for validation
tests := []struct {
name string
user *User
expectError bool
errorMsg string
}{
{
name: "valid user",
user: &User{
Username: "testuser",
Email: "test@example.com",
FullName: "Test User",
},
expectError: false,
},
{
name: "empty username",
user: &User{
Username: "",
Email: "test@example.com",
FullName: "Test User",
},
expectError: true,
errorMsg: "username cannot be empty",
},
{
name: "short username",
user: &User{
Username: "ab",
Email: "test@example.com",
FullName: "Test User",
},
expectError: true,
errorMsg: "username must be at least 3 characters",
},
{
name: "long username",
user: &User{
Username: "verylongusernamethatexceedslimit",
Email: "test@example.com",
FullName: "Test User",
},
expectError: true,
errorMsg: "username cannot exceed 20 characters",
},
{
name: "invalid email",
user: &User{
Username: "testuser",
Email: "invalid-email",
FullName: "Test User",
},
expectError: true,
errorMsg: "invalid email format",
},
{
name: "empty full name",
user: &User{
Username: "testuser",
Email: "test@example.com",
FullName: "",
},
expectError: true,
errorMsg: "full name cannot be empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := service.ValidateUser(tt.user)
if tt.expectError {
if err == nil {
t.Error("expected an error, got nil")
return
}
if err.Error() != tt.errorMsg {
t.Errorf("expected error %q, got %q", tt.errorMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("expected no error, got %v", err)
}
}
})
}
}
func TestUserService_CreateUser(t *testing.T) {
mockRepo := NewMockUserRepository()
mockEmail := NewMockEmailService()
service := NewUserService(mockRepo, mockEmail)
t.Run("successful creation", func(t *testing.T) {
user := &User{
Username: "newuser",
Email: "newuser@example.com",
FullName: "New User",
}
err := service.CreateUser(user)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
// Check user was created
if user.ID == 0 {
t.Error("user ID should be set after creation")
}
// Check welcome email was sent
emails := mockEmail.GetSentEmails()
if len(emails) != 1 {
t.Errorf("expected 1 email, got %d", len(emails))
}
expectedEmail := "welcome:" + user.Email
if emails[0] != expectedEmail {
t.Errorf("expected email %q, got %q", expectedEmail, emails[0])
}
})
t.Run("validation failure", func(t *testing.T) {
user := &User{
Username: "ab", // Too short
Email: "newuser@example.com",
FullName: "New User",
}
err := service.CreateUser(user)
if err == nil {
t.Error("expected validation error, got nil")
}
// Check no emails were sent
initialEmails := len(mockEmail.GetSentEmails())
if len(mockEmail.GetSentEmails()) != initialEmails {
t.Error("no emails should be sent for validation failure")
}
})
t.Run("duplicate username", func(t *testing.T) {
// Create first user
user1 := &User{
Username: "duplicate",
Email: "user1@example.com",
FullName: "User One",
}
err := service.CreateUser(user1)
if err != nil {
t.Fatalf("failed to create first user: %v", err)
}
// Try to create second user with same username
user2 := &User{
Username: "duplicate",
Email: "user2@example.com",
FullName: "User Two",
}
err = service.CreateUser(user2)
if err == nil {
t.Error("expected error for duplicate username, got nil")
}
if !strings.Contains(err.Error(), "username already exists") {
t.Errorf("expected 'username already exists' error, got %v", err)
}
})
t.Run("repository failure", func(t *testing.T) {
mockRepo.shouldFailCreate = true
defer func() { mockRepo.shouldFailCreate = false }()
user := &User{
Username: "failuser",
Email: "fail@example.com",
FullName: "Fail User",
}
err := service.CreateUser(user)
if err == nil {
t.Error("expected repository error, got nil")
}
if !strings.Contains(err.Error(), "failed to create user") {
t.Errorf("expected 'failed to create user' error, got %v", err)
}
})
}
func TestUserService_GetUser(t *testing.T) {
mockRepo := NewMockUserRepository()
service := NewUserService(mockRepo, nil)
// Create a test user
testUser := &User{
Username: "testuser",
Email: "test@example.com",
FullName: "Test User",
}
mockRepo.Create(testUser)
t.Run("successful get", func(t *testing.T) {
user, err := service.GetUser(testUser.ID)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if user.Username != testUser.Username {
t.Errorf("expected username %q, got %q", testUser.Username, user.Username)
}
})
t.Run("invalid ID", func(t *testing.T) {
_, err := service.GetUser(0)
if err == nil {
t.Error("expected error for invalid ID, got nil")
}
if err.Error() != "invalid user ID" {
t.Errorf("expected 'invalid user ID' error, got %v", err)
}
})
t.Run("user not found", func(t *testing.T) {
_, err := service.GetUser(999)
if err == nil {
t.Error("expected error for non-existent user, got nil")
}
if !strings.Contains(err.Error(), "failed to get user") {
t.Errorf("expected 'failed to get user' error, got %v", err)
}
})
}
5. Live Coding Demonstration (20-25 minutes)
Example 1: Benchmarking Performance
package performance
import (
"fmt"
"strings"
"testing"
)
// Different string concatenation methods to benchmark
func ConcatWithOperator(strs []string) string {
result := ""
for _, s := range strs {
result += s
}
return result
}
func ConcatWithBuilder(strs []string) string {
var builder strings.Builder
for _, s := range strs {
builder.WriteString(s)
}
return builder.String()
}
func ConcatWithPreAlloc(strs []string) string {
// Pre-calculate total length
totalLen := 0
for _, s := range strs {
totalLen += len(s)
}
var builder strings.Builder
builder.Grow(totalLen) // Pre-allocate
for _, s := range strs {
builder.WriteString(s)
}
return builder.String()
}
func ConcatWithJoin(strs []string) string {
return strings.Join(strs, "")
}
// Benchmark functions
func BenchmarkConcatWithOperator(b *testing.B) {
strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ConcatWithOperator(strs)
}
}
func BenchmarkConcatWithBuilder(b *testing.B) {
strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ConcatWithBuilder(strs)
}
}
func BenchmarkConcatWithPreAlloc(b *testing.B) {
strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ConcatWithPreAlloc(strs)
}
}
func BenchmarkConcatWithJoin(b *testing.B) {
strs := []string{"Hello", " ", "World", " ", "from", " ", "Go", " ", "benchmarks!"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ConcatWithJoin(strs)
}
}
// Benchmark with different sizes
func BenchmarkConcatSmall(b *testing.B) {
strs := []string{"a", "b", "c"}
b.Run("Operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithOperator(strs)
}
})
b.Run("Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithBuilder(strs)
}
})
b.Run("Join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithJoin(strs)
}
})
}
func BenchmarkConcatLarge(b *testing.B) {
strs := make([]string, 1000)
for i := range strs {
strs[i] = fmt.Sprintf("string_%d", i)
}
b.Run("Operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithOperator(strs)
}
})
b.Run("Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithBuilder(strs)
}
})
b.Run("PreAlloc", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithPreAlloc(strs)
}
})
b.Run("Join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ConcatWithJoin(strs)
}
})
}
// Memory allocation benchmarks
func BenchmarkConcatMemory(b *testing.B) {
strs := make([]string, 100)
for i := range strs {
strs[i] = fmt.Sprintf("str_%d", i)
}
b.Run("Operator", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ConcatWithOperator(strs)
}
})
b.Run("Builder", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ConcatWithBuilder(strs)
}
})
b.Run("PreAlloc", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ConcatWithPreAlloc(strs)
}
})
}
Example 2: Integration Testing with HTTP Handlers
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"full_name"`
}
type UserAPI struct {
service UserService
}
func NewUserAPI(service UserService) *UserAPI {
return &UserAPI{service: service}
}
func (api *UserAPI) GetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, err := api.service.GetUser(id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
http.Error(w, "User not found", http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (api *UserAPI) CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := api.service.CreateUser(&user); err != nil {
if strings.Contains(err.Error(), "validation") {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if strings.Contains(err.Error(), "already exists") {
http.Error(w, err.Error(), http.StatusConflict)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (api *UserAPI) UpdateUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
user.ID = id
if err := api.service.UpdateUser(&user); err != nil {
if strings.Contains(err.Error(), "validation") {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if strings.Contains(err.Error(), "not found") {
http.Error(w, "User not found", http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (api *UserAPI) SetupRoutes() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/users/{id:[0-9]+}", api.GetUser).Methods("GET")
r.HandleFunc("/users", api.CreateUser).Methods("POST")
r.HandleFunc("/users/{id:[0-9]+}", api.UpdateUser).Methods("PUT")
return r
}
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestUserAPI_GetUser(t *testing.T) {
mockRepo := NewMockUserRepository()
mockEmail := NewMockEmailService()
service := NewUserService(mockRepo, mockEmail)
api := NewUserAPI(service)
// Create test user
testUser := &User{
Username: "testuser",
Email: "test@example.com",
FullName: "Test User",
}
service.CreateUser(testUser)
router := api.SetupRoutes()
t.Run("successful get", func(t *testing.T) {
req := httptest.NewRequest("GET", fmt.Sprintf("/users/%d", testUser.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
}
var response User
err := json.NewDecoder(w.Body).Decode(&response)
if err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if response.Username != testUser.Username {
t.Errorf("expected username %q, got %q", testUser.Username, response.Username)
}
})
t.Run("user not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
}
})
t.Run("invalid user ID", func(t *testing.T) {
req := httptest.NewRequest("GET", "/users/invalid", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound { // Mux will return 404 for non-matching route
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
}
})
}
func TestUserAPI_CreateUser(t *testing.T) {
mockRepo := NewMockUserRepository()
mockEmail := NewMockEmailService()
service := NewUserService(mockRepo, mockEmail)
api := NewUserAPI(service)
router := api.SetupRoutes()
t.Run("successful creation", func(t *testing.T) {
user := User{
Username: "newuser",
Email: "new@example.com",
FullName: "New User",
}
body, _ := json.Marshal(user)
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("expected status %d, got %d", http.StatusCreated, w.Code)
}
var response User
err := json.NewDecoder(w.Body).Decode(&response)
if err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if response.ID == 0 {
t.Error("expected user ID to be set")
}
if response.Username != user.Username {
t.Errorf("expected username %q, got %q", user.Username, response.Username)
}
})
t.Run("validation error", func(t *testing.T) {
user := User{
Username: "ab", // Too short
Email: "new@example.com",
FullName: "New User",
}
body, _ := json.Marshal(user)
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
}
})
t.Run("invalid JSON", func(t *testing.T) {
req := httptest.NewRequest("POST", "/users", bytes.NewReader([]byte("invalid json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
}
})
}
func TestUserAPI_UpdateUser(t *testing.T) {
mockRepo := NewMockUserRepository()
mockEmail := NewMockEmailService()
service := NewUserService(mockRepo, mockEmail)
api := NewUserAPI(service)
// Create test user
testUser := &User{
Username: "testuser",
Email: "test@example.com",
FullName: "Test User",
}
service.CreateUser(testUser)
router := api.SetupRoutes()
t.Run("successful update", func(t *testing.T) {
updatedUser := User{
Username: "updateduser",
Email: "updated@example.com",
FullName: "Updated User",
}
body, _ := json.Marshal(updatedUser)
req := httptest.NewRequest("PUT", fmt.Sprintf("/users/%d", testUser.ID), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
}
var response User
err := json.NewDecoder(w.Body).Decode(&response)
if err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if response.Username != updatedUser.Username {
t.Errorf("expected username %q, got %q", updatedUser.Username, response.Username)
}
})
t.Run("user not found", func(t *testing.T) {
user := User{
Username: "notfound",
Email: "notfound@example.com",
FullName: "Not Found",
}
body, _ := json.Marshal(user)
req := httptest.NewRequest("PUT", "/users/999", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
}
})
}
// Benchmark API endpoints
func BenchmarkUserAPI_GetUser(b *testing.B) {
mockRepo := NewMockUserRepository()
mockEmail := NewMockEmailService()
service := NewUserService(mockRepo, mockEmail)
api := NewUserAPI(service)
// Create test user
testUser := &User{
Username: "benchuser",
Email: "bench@example.com",
FullName: "Bench User",
}
service.CreateUser(testUser)
router := api.SetupRoutes()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", fmt.Sprintf("/users/%d", testUser.ID), nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
b.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
}
}
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: E-commerce Testing Suite (10 minutes)
Objective: Write comprehensive tests for a shopping cart system
Instructions:
- Create unit tests for cart operations (add, remove, update quantities)
- Write table-driven tests for price calculations
- Test edge cases like empty carts and invalid items
- Benchmark different discount calculation algorithms
Starter Code:
type ShoppingCart struct {
Items []CartItem
}
type CartItem struct {
ProductID int
Quantity int
Price float64
}
func (c *ShoppingCart) AddItem(productID int, quantity int, price float64) error {
// TODO: Implement
return nil
}
func (c *ShoppingCart) GetTotal() float64 {
// TODO: Implement
return 0.0
}
func TestShoppingCart_AddItem(t *testing.T) {
// TODO: Write comprehensive tests
}
Exercise 2: HTTP Client Testing (15 minutes)
Objective: Test an HTTP client that consumes external APIs
Requirements:
- Mock HTTP responses using httptest
- Test different status codes and error conditions
- Verify request headers and payloads
- Test timeout and retry logic
Hints:
- Use httptest.NewServer for creating test servers
- Test both success and failure scenarios
- Consider using testify/assert for cleaner assertions
Exercise 3: Concurrent Code Testing (15 minutes)
Objective: Test concurrent operations safely
Challenge: Write tests for:
- Race condition detection
- Goroutine leak prevention
- Channel communication patterns
- Worker pool implementations
Extension Ideas:
- Use go test -race to detect race conditions
- Implement stress tests with multiple goroutines
- Test graceful shutdown scenarios
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
- Problem: Tests that depend on external services Cause: Not properly mocking dependencies Solution: Use interfaces and dependency injection
// Wrong - depends on external database
func TestUserService_GetUser(t *testing.T) {
db, _ := sql.Open("postgres", "host=localhost...")
service := NewUserService(db)
// Test will fail if database is unavailable
}
// Right - uses mock
func TestUserService_GetUser(t *testing.T) {
mockRepo := NewMockUserRepository()
service := NewUserService(mockRepo)
// Test is isolated and reliable
}
- Problem: Non-deterministic tests Cause: Depending on time, random values, or order of operations Solution: Control inputs and use fixed values
// Wrong - depends on current time
func TestExpiration(t *testing.T) {
token := CreateToken(time.Now().Add(time.Hour))
if token.IsExpired() {
t.Error("token should not be expired")
}
}
// Right - uses fixed time
func TestExpiration(t *testing.T) {
fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
token := CreateToken(fixedTime.Add(time.Hour))
if token.IsExpiredAt(fixedTime) {
t.Error("token should not be expired")
}
}
Debugging Tips
- Use
t.Helper()in test helper functions - Run tests with
-vflag for verbose output - Use
t.Parallel()for independent tests - Consider using testify for better assertions and test setup
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Test Naming: Use descriptive names that explain the scenario
- Test Organization: Group related tests using subtests
- Coverage: Aim for meaningful coverage, not just high percentages
- Documentation: Write tests that serve as documentation
Performance Testing
func BenchmarkFunction(b *testing.B) {
// Setup code here (not timed)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Code to benchmark
result := ExpensiveFunction()
_ = result // Prevent optimization
}
}
Test Coverage Analysis
# Run tests with coverage
go test -cover
# Generate detailed coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
Next Steps in Your Learning Journey
- Learn property-based testing with tools like gopter
- Explore mutation testing for test quality assessment
- Study advanced profiling techniques
- Practice with real-world testing scenarios
9. Summary and Review (5 minutes)
Key Takeaways
- Go’s testing package provides powerful tools for unit tests, benchmarks, and examples
- Table-driven tests enable comprehensive scenario coverage efficiently
- Mocking is essential for isolated, reliable unit tests
- Benchmarking helps identify and fix performance issues
Quick Review Questions
- What’s the difference between
t.Errorandt.Fatal? - How do you structure table-driven tests effectively?
- When should you use benchmarks vs. profiling?
For Instructors
Teaching Tips
- Demonstrate test-driven development (TDD) workflow
- Show both positive and negative test cases
- Emphasize the importance of test maintainability
- Use real examples from production code
Assessment Ideas
- Build a complete test suite for a web service
- Implement performance optimization based on benchmark results
- Create integration tests for a database-backed application
Adaptation Notes
- For beginners: Focus on unit testing fundamentals and basic benchmarking
- For advanced students: Include property-based testing and advanced profiling
- Online delivery: Provide sample codebases for testing practice