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.Errorf、t.Fatalf 或 t.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) | 測試執行速度變慢且不穩定 | 使用 mock、interface 抽象或 testcontainers;必要時才使用 TestMain |
在測試中使用 t.Fatalf 但仍想檢查其他斷言 |
Fatal 會立即停止測試,導致後續檢查被略過 |
只在「無法繼續」的情況下使用 Fatal,其他情況使用 Error |
| 基準測試忘記重置全域變數 | 可能導致每次迭代結果不同,基準數據失真 | 在基準測試內部 重新初始化 所有狀態,或使用 b.ResetTimer() |
| 測試案例過於冗長,缺乏可讀性 | 每個測試函式寫太多邏輯,難以維護 | 採用 table‑driven + 子測試,保持每筆案例簡潔 |
其他最佳實踐
- 保持測試獨立:每個測試案例不應依賴其他測試的執行結果。
- 使用
go test -cover監控測試覆蓋率,目標至少 80%(視專案而定)。 - 在 CI pipeline 中加入
go test ./... -race,檢測競爭條件(race condition)。 - 命名要具描述性:
TestParseConfig_ValidFile、TestParseConfig_InvalidJSON能立即讓人了解測試目的。 - 避免在測試中使用
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 語言內建、輕量且功能完整的測試框架,零設定即可開始寫測試。- 掌握 基本斷言、表格驅動測試、子測試、基準測試,能讓測試程式碼保持簡潔且易於擴充。
- 透過
TestMain、httptest、mock 以及 CI 整合,可把測試提升到 自動化、可靠 的層級。 - 避免常見陷阱(檔名錯誤、測試相依外部資源、過度使用 Fatal)並遵循最佳實踐(獨立測試、覆蓋率監控、競爭條件檢測),可以大幅提升專案的品質與維護效率。
把 寫程式 的同時養成 寫測試 的習慣,長遠來看不僅能減少 bug,也能讓程式碼在重構或功能擴充時更有信心。現在就打開你的專案,新增第一個 _test.go,讓測試成為開發流程中不可或缺的一環吧!