Tech Blog

Exploring Technology, Innovation & Future

繁體中文ProgrammingJuly 15, 2025

Go語言測試最佳實踐:單元測試與效能測試

測試是軟體開發中不可或缺的一環,Go語言內建了優秀的測試框架,讓開發者能夠輕鬆編寫和執行測試。本文將深入探討Go語言中的測試最佳實踐,包括單元測試、基準測試和模擬測試。

Go測試基礎

測試檔案結構 Go的測試檔案需要遵循特定的命名規則: - 測試檔案名必須以 `_test.go` 結尾 - 測試函數必須以 `Test` 開頭 - 測試函數的簽名必須是 `func TestXxx(t *testing.T)`

範例檔案結構: ``` project/ ├── math.go ├── math_test.go ├── user.go └── user_test.go ```

基本單元測試

被測試的程式碼 (math.go): ```go package math

func Add(a, b int) int { return a + b }

func Multiply(a, b int) int { return a * b }

func Divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("不能除以零") } return a / b, nil } ```

對應的測試 (math_test.go): ```go package math

import ( "testing" )

func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5 if result != expected { t.Errorf("Add(2, 3) = %d; 期望 %d", result, expected) } }

func TestMultiply(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"正數相乘", 3, 4, 12}, {"負數相乘", -2, 3, -6}, {"零相乘", 0, 5, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Multiply(tt.a, tt.b) if result != tt.expected { t.Errorf("Multiply(%d, %d) = %d; 期望 %d", tt.a, tt.b, result, tt.expected) } }) } }

func TestDivide(t *testing.T) { // 正常情況 result, err := Divide(10, 2) if err != nil { t.Errorf("意外的錯誤: %v", err) } if result != 5.0 { t.Errorf("Divide(10, 2) = %f; 期望 5.0", result) } // 錯誤情況 _, err = Divide(10, 0) if err == nil { t.Error("期望發生錯誤,但沒有錯誤") } } ```

表格驅動測試

表格驅動測試是Go中的常見模式,適合測試多個輸入輸出組合:

```go func TestIsValidEmail(t *testing.T) { tests := []struct { name string email string expected bool }{ {"有效郵箱", "[email protected]", true}, {"無效郵箱 - 缺少@", "userexample.com", false}, {"無效郵箱 - 缺少域名", "user@", false}, {"空字串", "", false}, {"特殊字符", "[email protected]", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsValidEmail(tt.email) if result != tt.expected { t.Errorf("IsValidEmail(%q) = %v; 期望 %v", tt.email, result, tt.expected) } }) } } ```

測試HTTP處理器

測試HTTP端點 ```go func TestGetUsersHandler(t *testing.T) { // 創建測試請求 req, err := http.NewRequest("GET", "/users", nil) if err != nil { t.Fatal(err) } // 創建ResponseRecorder來記錄響應 rr := httptest.NewRecorder() handler := http.HandlerFunc(GetUsersHandler) // 調用處理器 handler.ServeHTTP(rr, req) // 檢查狀態碼 if status := rr.Code; status != http.StatusOK { t.Errorf("錯誤的狀態碼: 得到 %v 期望 %v", status, http.StatusOK) } // 檢查響應體 expected := `{"users":[]}` if rr.Body.String() != expected { t.Errorf("錯誤的響應體: 得到 %v 期望 %v", rr.Body.String(), expected) } } ```

使用Gin框架的測試 ```go func TestGinGetUsers(t *testing.T) { // 設置Gin為測試模式 gin.SetMode(gin.TestMode) // 創建路由 r := gin.Default() r.GET("/users", GetUsers) // 創建測試請求 w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/users", nil) // 執行請求 r.ServeHTTP(w, req) // 斷言 assert.Equal(t, 200, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.Nil(t, err) assert.Contains(t, response, "data") } ```

Mock和存根測試

使用介面進行測試隔離 ```go // 定義介面 type UserRepository interface { GetUser(id int) (*User, error) SaveUser(user *User) error }

// 實際實現 type DatabaseUserRepository struct { db *sql.DB }

func (r *DatabaseUserRepository) GetUser(id int) (*User, error) { // 實際資料庫操作 return nil, nil }

// 測試用的Mock實現 type MockUserRepository struct { users map[int]*User }

func (m *MockUserRepository) GetUser(id int) (*User, error) { if user, exists := m.users[id]; exists { return user, nil } return nil, errors.New("用戶不存在") }

func (m *MockUserRepository) SaveUser(user *User) error { m.users[user.ID] = user return nil }

// 服務層 type UserService struct { repo UserRepository }

func (s *UserService) GetUserByID(id int) (*User, error) { return s.repo.GetUser(id) }

// 測試 func TestUserService_GetUserByID(t *testing.T) { // 創建Mock mockRepo := &MockUserRepository{ users: map[int]*User{ 1: {ID: 1, Name: "張三", Email: "[email protected]"}, }, } service := &UserService{repo: mockRepo} // 測試成功情況 user, err := service.GetUserByID(1) if err != nil { t.Errorf("意外的錯誤: %v", err) } if user.Name != "張三" { t.Errorf("期望用戶名為 '張三',但得到 '%s'", user.Name) } // 測試失敗情況 _, err = service.GetUserByID(999) if err == nil { t.Error("期望發生錯誤,但沒有錯誤") } } ```

基準測試 (Benchmark)

基本基準測試 ```go func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } }

func BenchmarkStringConcat(b *testing.B) { for i := 0; i < b.N; i++ { result := "" for j := 0; j < 100; j++ { result += "hello" } } }

func BenchmarkStringBuilder(b *testing.B) { for i := 0; i < b.N; i++ { var builder strings.Builder for j := 0; j < 100; j++ { builder.WriteString("hello") } _ = builder.String() } } ```

執行基準測試 ```bash # 執行所有基準測試 go test -bench=.

# 執行特定基準測試 go test -bench=BenchmarkAdd

# 顯示記憶體分配統計 go test -bench=. -benchmem

# 多次執行以獲得更準確的結果 go test -bench=. -count=3 ```

測試覆蓋率

生成覆蓋率報告 ```bash # 生成覆蓋率報告 go test -cover

# 生成詳細的覆蓋率文件 go test -coverprofile=coverage.out

# 查看覆蓋率報告 go tool cover -html=coverage.out

# 按函數顯示覆蓋率 go tool cover -func=coverage.out ```

集成測試

資料庫集成測試 ```go func TestUserRepository_Integration(t *testing.T) { // 跳過短測試 if testing.Short() { t.Skip("跳過集成測試") } // 設置測試資料庫 db := setupTestDB(t) defer teardownTestDB(t, db) repo := &DatabaseUserRepository{db: db} // 測試保存用戶 user := &User{Name: "測試用戶", Email: "[email protected]"} err := repo.SaveUser(user) if err != nil { t.Fatalf("保存用戶失敗: %v", err) } // 測試獲取用戶 retrievedUser, err := repo.GetUser(user.ID) if err != nil { t.Fatalf("獲取用戶失敗: %v", err) } if retrievedUser.Name != user.Name { t.Errorf("期望用戶名 %s,但得到 %s", user.Name, retrievedUser.Name) } }

func setupTestDB(t *testing.T) *sql.DB { // 設置測試資料庫連接 db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatalf("無法連接測試資料庫: %v", err) } // 創建表格 _, err = db.Exec(`CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, email TEXT )`) if err != nil { t.Fatalf("無法創建測試表格: %v", err) } return db }

func teardownTestDB(t *testing.T, db *sql.DB) { db.Close() } ```

測試最佳實踐

1. 測試命名 ```go // 好的命名 - 描述性強 func TestUserService_CreateUser_WithValidData_ReturnsUser(t *testing.T) {} func TestUserService_CreateUser_WithInvalidEmail_ReturnsError(t *testing.T) {}

// 避免的命名 - 不夠具體 func TestCreateUser(t *testing.T) {} func TestCreateUser2(t *testing.T) {} ```

2. 測試隔離 ```go func TestUserOperations(t *testing.T) { t.Run("CreateUser", func(t *testing.T) { // 每個子測試都是獨立的 user := createTestUser() // 測試邏輯 }) t.Run("UpdateUser", func(t *testing.T) { // 獨立的測試環境 user := createTestUser() // 測試邏輯 }) } ```

3. 使用測試輔助函數 ```go func createTestUser() *User { return &User{ ID: 1, Name: "測試用戶", Email: "[email protected]", } }

func assertUserEqual(t *testing.T, expected, actual *User) { t.Helper() // 標記為輔助函數 if expected.Name != actual.Name { t.Errorf("期望用戶名 %s,但得到 %s", expected.Name, actual.Name) } if expected.Email != actual.Email { t.Errorf("期望郵箱 %s,但得到 %s", expected.Email, actual.Email) } } ```

Go語言的測試框架簡單而強大,通過遵循這些最佳實踐,你可以編寫出高品質、可維護的測試程式碼,確保你的應用程式的可靠性和穩定性。記住,好的測試不僅能發現bug,還能作為程式碼的活文檔,幫助其他開發者理解程式碼的預期行為。

Related Articles