本文 AI 產出,尚未審核

Golang 單元測試(testing 包)

簡介

在軟體開發的全流程中,測試是保證程式品質、降低維護成本的關鍵環節。Go 語言自帶的 testing 套件提供了輕量且功能完整的單元測試框架,讓開發者能以最少的設定,快速驗證函式或方法的行為是否符合預期。

對於 初學者,掌握 testing 的基本用法可以在寫程式的同時養成測試的好習慣;對 中級開發者,則能藉由進階技巧(如 table‑driven tests、子測試、基準測試)提升測試覆蓋率與可讀性,進一步支援持續整合(CI)與自動化部署。

本篇文章將從核心概念出發,結合實務範例,說明如何在 Go 專案中建立、執行與管理單元測試,並分享常見陷阱與最佳實踐,協助你在日常開發中自然地加入測試流程。


核心概念

1. testing 套件的基本結構

Go 的測試檔案必須以 _test.go 為副檔名,且每個測試函式必須以 Test 為前綴,接受 *testing.T 參數。例如:

package calc

import "testing"

func TestAdd(t *testing.T) {
    // 測試內容放這裡
}
  • *testing.T 提供了錯誤回報、測試失敗、跳過測試等方法。
  • 執行測試時,只要在專案根目錄執行 go test ./...,Go 會自動編譯並執行所有符合命名規則的測試。

2. 斷言(assert)與錯誤回報

Go 標準庫沒有內建的斷言函式,通常直接使用 t.Errorft.Fatalft.FailNow 來報告不符合預期的情況。以下範例展示了兩種常見寫法:

func TestMultiply(t *testing.T) {
    got := Multiply(3, 4)
    want := 12

    if got != want {
        t.Errorf("Multiply(3,4) = %d; want %d", got, want) // 測試失敗但繼續執行
    }
}

如果錯誤足以讓後續測試無意義,使用 t.Fatalf 立即終止:

func TestDivide(t *testing.T) {
    result, err := Divide(10, 0)
    if err == nil {
        t.Fatalf("expected error when dividing by zero, got nil")
    }
    // 只有在有錯誤時才檢查 result
    if result != 0 {
        t.Errorf("result should be 0 when error occurs, got %d", result)
    }
}

3. Table‑Driven Tests(表格驅動測試)

當同一個函式需要測試多組輸入/輸出時,使用 表格驅動測試 可以讓程式碼更簡潔、易於擴充。範例如下:

func TestIsEven(t *testing.T) {
    tests := []struct {
        name string
        in   int
        want bool
    }{
        {"偶數 2", 2, true},
        {"奇數 3", 3, false},
        {"零 0", 0, true},
        {"負偶數 -4", -4, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := IsEven(tt.in); got != tt.want {
                t.Errorf("IsEven(%d) = %v; want %v", tt.in, got, tt.want)
            }
        })
    }
}
  • t.Run 會為每筆測試案例建立 子測試,在測試報告中能清楚看到每個案例的通過或失敗情況。
  • 這種寫法在 新增測試案例 時只需要在 tests 陣列中加入一筆資料,維護成本極低。

4. 基準測試(Benchmark)

除了驗證功能正確性,testing 也支援效能基準測試。只要以 Benchmark 為前綴,接受 *testing.B 參數即可:

func BenchmarkFibRecursive(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibRecursive(20) // 只要保證每次執行的結果相同即可
    }
}
  • b.N 由測試框架自動調整,確保測試時間足夠長以得到穩定的結果。
  • 使用 -benchmem 參數可以同時觀察記憶體分配情況,對優化演算法非常有幫助。

5. 測試套件的初始化與清理(TestMain)

有時候測試前需要建立測試資料庫、啟動測試伺服器,測試結束後則需要清理資源。此時可以實作 TestMain

func TestMain(m *testing.M) {
    // 1. 設定測試環境(例如連線測試資料庫)
    db, err := sql.Open("postgres", "user=test dbname=testdb sslmode=disable")
    if err != nil {
        fmt.Println("setup db failed:", err)
        os.Exit(1)
    }
    // 將 db 存入全域變數供其他測試使用
    testDB = db

    // 2. 執行所有測試
    code := m.Run()

    // 3. 清理資源
    db.Close()
    os.Exit(code)
}

TestMain 只會被執行一次,適合做 一次性的初始化與清理,避免在每個測試函式裡重複程式碼。


常見陷阱與最佳實踐

常見陷阱 為何會發生 解決方式 / 最佳實踐
測試檔案忘記加 _test.go go test 只會編譯符合命名規則的檔案 建立 Git hook 或 IDE 模板,確保檔名正確
測試相依外部資源(DB、API) 測試執行速度變慢且不穩定 使用 mockinterface 抽象或 testcontainers;必要時才使用 TestMain
在測試中使用 t.Fatalf 但仍想檢查其他斷言 Fatal 會立即停止測試,導致後續檢查被略過 只在「無法繼續」的情況下使用 Fatal,其他情況使用 Error
基準測試忘記重置全域變數 可能導致每次迭代結果不同,基準數據失真 在基準測試內部 重新初始化 所有狀態,或使用 b.ResetTimer()
測試案例過於冗長,缺乏可讀性 每個測試函式寫太多邏輯,難以維護 採用 table‑driven + 子測試,保持每筆案例簡潔

其他最佳實踐

  1. 保持測試獨立:每個測試案例不應依賴其他測試的執行結果。
  2. 使用 go test -cover 監控測試覆蓋率,目標至少 80%(視專案而定)。
  3. 在 CI pipeline 中加入 go test ./... -race,檢測競爭條件(race condition)。
  4. 命名要具描述性TestParseConfig_ValidFileTestParseConfig_InvalidJSON 能立即讓人了解測試目的。
  5. 避免在測試中使用 log.Print,除非真的需要除錯;測試失敗時 t.Log 會自動顯示訊息。

實際應用場景

1. API 服務的請求驗證

在 RESTful API 中,常見的需求是驗證傳入的 JSON 結構與欄位限制。使用 testing 搭配 httptest 套件可以在 單元測試 中模擬 HTTP 請求,確保路由、驗證與回應皆符合規範。

func TestCreateUserHandler(t *testing.T) {
    // 1. 準備測試請求
    payload := `{"name":"Alice","email":"alice@example.com"}`
    req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    // 2. 呼叫處理函式
    CreateUserHandler(w, req)

    // 3. 驗證回應
    resp := w.Result()
    if resp.StatusCode != http.StatusCreated {
        t.Fatalf("expected status 201, got %d", resp.StatusCode)
    }
    var respBody struct{ ID int `json:"id"` }
    json.NewDecoder(resp.Body).Decode(&respBody)
    if respBody.ID == 0 {
        t.Errorf("expected non‑zero user ID")
    }
}

2. 演算法效能比較

在需要挑選最佳演算法的情境(例如排序、搜尋),可以同時寫 功能測試基準測試,快速比較不同實作的效能差異。

func BenchmarkSortQuick(b *testing.B) {
    data := generateRandomSlice(10000)
    for i := 0; i < b.N; i++ {
        // 每次都要複製原始資料,避免已排序的影響
        tmp := append([]int(nil), data...)
        QuickSort(tmp)
    }
}

透過 go test -bench=. -benchmem,團隊可以在 PR 中直接看到效能回歸或改進的數據。

3. 持續整合(CI)中的自動測試

在 GitHub Actions、GitLab CI 或 Jenkins 中,只要加入以下步驟,即可在每次提交時自動執行測試與覆蓋率檢查:

# .github/workflows/go.yml
name: Go CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.22'
      - name: Run tests
        run: |
          go test ./... -v -coverprofile=coverage.out
          go tool cover -func=coverage.out

這樣即使是 新手開發者,也能在提交前看到測試結果,避免錯誤代碼進入主分支。


總結

  • testing 是 Go 語言內建、輕量且功能完整的測試框架,零設定即可開始寫測試。
  • 掌握 基本斷言、表格驅動測試、子測試、基準測試,能讓測試程式碼保持簡潔且易於擴充。
  • 透過 TestMainhttptest、mock 以及 CI 整合,可把測試提升到 自動化、可靠 的層級。
  • 避免常見陷阱(檔名錯誤、測試相依外部資源、過度使用 Fatal)並遵循最佳實踐(獨立測試、覆蓋率監控、競爭條件檢測),可以大幅提升專案的品質與維護效率。

寫程式 的同時養成 寫測試 的習慣,長遠來看不僅能減少 bug,也能讓程式碼在重構或功能擴充時更有信心。現在就打開你的專案,新增第一個 _test.go,讓測試成為開發流程中不可或缺的一環吧!