Go 測試的核心規則是:測試檔以 _test.go 結尾,測試函式以 Test 開頭並接收 *testing.T。本章將說明如何建立第一個單元測試、檢查結果與回報失敗。

測試是同一個 package 的行為說明

Go 測試的核心目標是驗證可觀察行為。測試用輸入、輸出與錯誤條件說明這段程式應該如何運作。

1func NormalizeName(input string) string {
2    input = strings.TrimSpace(input)
3    return strings.ToLower(input)
4}

這個函式的可觀察行為是:移除前後空白,並轉成小寫。測試應該檢查這個行為,而不是檢查函式內部是否真的先呼叫 TrimSpace 再呼叫 ToLower

1func TestNormalizeName(t *testing.T) {
2    got := NormalizeName("  Alice  ")
3    want := "alice"
4
5    if got != want {
6        t.Fatalf("NormalizeName() = %q, want %q", got, want)
7    }
8}

這就是最小的 Go 單元測試:準備輸入,呼叫函式,比對結果,失敗時回報清楚訊息。

測試檔命名有固定規則

Go 測試檔的核心規則是檔名必須以 _test.go 結尾。測試函式必須以 Test 開頭,接收一個 *testing.T 參數,且沒有回傳值。

1// normalize_test.go
2func TestNormalizeName(t *testing.T) {
3    // ...
4}

go test 會自動找到這些檔案與函式。測試檔可以和被測程式放在同一個 package,也可以使用 package xxx_test 建立外部測試 package。

同 package 測試可以存取未匯出的函式與型別,外部測試只能使用匯出的 API。入門階段可以先用同 package 測試,等到需要從使用者視角驗證 public API 時,再使用外部測試。

失敗訊息要說明 got 與 want

測試失敗訊息的核心責任是幫助讀者快速定位差異。Go 社群常用 gotwant 表示實際結果與預期結果。

1if got != want {
2    t.Fatalf("NormalizeName() = %q, want %q", got, want)
3}

這個訊息包含函式名稱、實際結果與預期結果。當測試失敗時,讀者不需要再打開測試檔猜哪個值錯了。

t.Fatalt.Fatalf 會立刻中止目前測試;t.Errort.Errorf 會記錄失敗但繼續執行。若後續檢查依賴目前結果,使用 Fatalf 比較安全。

1got, err := ParsePort("8080")
2if err != nil {
3    t.Fatalf("ParsePort() error = %v", err)
4}
5
6if got != 8080 {
7    t.Fatalf("ParsePort() = %d, want %d", got, 8080)
8}

如果解析已經失敗,後面再檢查數值沒有意義,所以先用 Fatalf 結束測試。

測試錯誤要明確檢查錯誤存在

錯誤情境測試的核心原則是同時檢查「是否有錯」與「錯誤是否符合預期」。只檢查回傳值常常不足以描述失敗行為。

 1func ParsePort(input string) (int, error) {
 2    port, err := strconv.Atoi(input)
 3    if err != nil {
 4        return 0, fmt.Errorf("parse port %q: %w", input, err)
 5    }
 6
 7    if port <= 0 {
 8        return 0, fmt.Errorf("port must be positive")
 9    }
10
11    return port, nil
12}

測試成功情境時,應確認沒有錯誤;測試失敗情境時,應確認錯誤確實發生。

1func TestParsePortInvalid(t *testing.T) {
2    _, err := ParsePort("abc")
3    if err == nil {
4        t.Fatalf("ParsePort() error = nil, want error")
5    }
6}

若程式使用 sentinel error 或可辨識的錯誤型別,可以再用 errors.Iserrors.As 檢查錯誤種類。不要只比對完整錯誤字串,除非錯誤訊息本身就是公開合約。

helper 函式可以降低重複

測試 helper 的核心用途是隱藏準備資料的細節,而不是隱藏真正的驗證邏輯。helper 應該讓測試主體更接近「這個行為應該成立」。

 1func mustParsePort(t *testing.T, input string) int {
 2    t.Helper()
 3
 4    port, err := ParsePort(input)
 5    if err != nil {
 6        t.Fatalf("ParsePort(%q) error = %v", input, err)
 7    }
 8
 9    return port
10}

t.Helper() 會讓失敗行號指向呼叫 helper 的測試,而不是 helper 內部。這能讓測試失敗時更快找到真正的案例位置。

helper 不應該把測試意圖藏起來。若 helper 名稱太抽象,或讀者必須跳進 helper 才知道測試在驗證什麼,這個 helper 可能反而降低可讀性。

測試要避免依賴不穩定環境

可靠測試的核心規則是讓輸入可控制、輸出可觀察。時間、隨機數、檔案系統、網路與全域狀態都可能讓測試不穩定。

1func IsExpired(now time.Time, deadline time.Time) bool {
2    return now.After(deadline)
3}

這個函式把 now 當成參數,因此測試可以傳入固定時間。

1func TestIsExpired(t *testing.T) {
2    now := time.Date(2026, 4, 22, 10, 0, 0, 0, time.UTC)
3    deadline := time.Date(2026, 4, 22, 9, 0, 0, 0, time.UTC)
4
5    if !IsExpired(now, deadline) {
6        t.Fatalf("IsExpired() = false, want true")
7    }
8}

測試要在合適層級控制變因。單元測試優先控制依賴,整合測試才使用真實檔案、網路或服務。

下一章

下一章會介紹 table-driven test,說明如何用同一個測試流程整理多組案例。