本文 AI 產出,尚未審核

Golang 測試與除錯:覆蓋率測試(Coverage)

簡介

在軟體開發的全流程中,測試是保證程式品質的關鍵步驟。即使寫了大量的單元測試,若沒有量化測試的覆蓋範圍,仍然難以判斷哪些程式碼路徑真的被驗證過。Go 語言內建的 go test -cover 功能,讓開發者可以快速得到程式碼覆蓋率的統計資訊,並以圖形化的方式呈現未被測試的區塊。透過覆蓋率測試,我們可以:

  • 發現盲點:找出未被測試的函式或條件分支。
  • 提升測試品質:在保持測試成本的同時,逐步提升測試完整度。
  • 支援持續整合:在 CI/CD pipeline 中加入覆蓋率門檻,防止回歸缺失。

本篇文章將從概念、實作、常見陷阱與最佳實踐,逐步帶你掌握 Go 的覆蓋率測試,並提供可直接套用的範例程式碼。


核心概念

1. 為什麼要測量覆蓋率?

  • 覆蓋率 ≠ 完整性:即使覆蓋率達到 100%,仍可能缺少邊界值測試或錯誤處理測試。
  • 量化指標:覆蓋率提供一個可視化的指標,協助團隊討論測試範圍與優先級。
  • 風險管理:在關鍵模組(如金融、醫療)上設定較高的覆蓋門檻,可降低發佈風險。

2. Go 的覆蓋率工具

指令 說明
go test -cover 執行測試並在終端機輸出總體覆蓋率(%)。
go test -coverprofile=coverage.out 產生覆蓋率資料檔 coverage.out,可供後續分析。
go tool cover -html=coverage.out -o coverage.html coverage.out 轉成 HTML,直接在瀏覽器看到每行程式碼的顏色標示。
go test -covermode=count 使用「計數」模式,能顯示每行被執行的次數,適合分析重複執行的路徑。

小技巧:在 CI 中加入 -coverpkg=./...,可以一次測量所有子套件的覆蓋率。

3. 覆蓋率的三種模式

模式 產生的資料 適用情境
set (預設) 每行程式碼是否被執行過 快速檢查是否有遺漏
count 每行被執行的次數 分析熱點與測試重複度
atomic 使用原子操作計算覆蓋,支援並行測試 大型專案的 CI/CD,避免競爭條件

程式碼範例

以下範例以一個簡單的 計算機 套件 calc 為例,示範如何寫測試、產生覆蓋率報告,以及解讀結果。

1. 基礎函式與測試

// calc/add.go
package calc

// Add 回傳兩個整數的加總
func Add(a, b int) int {
    return a + b
}
// calc/add_test.go
package calc

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    if got != 5 {
        t.Fatalf("Add(2,3) = %d; want 5", got)
    }
}

執行 go test -cover

$ go test ./calc -cover
ok  	./calc	0.012s	coverage: 100.0% of statements

這裡的 100% 代表 Add 函式的每一行都被測試執行過。

2. 多分支函式的覆蓋率

// calc/divide.go
package calc

import "errors"

// Divide 回傳 a 除以 b,b 為 0 時回傳錯誤
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
// calc/divide_test.go
package calc

import "testing"

func TestDivide(t *testing.T) {
    // 正常情況
    if got, _ := Divide(10, 2); got != 5 {
        t.Fatalf("Divide(10,2) = %d; want 5", got)
    }

    // 錯誤情況
    if _, err := Divide(10, 0); err == nil {
        t.Fatalf("expected error for division by zero")
    }
}

執行覆蓋率:

$ go test ./calc -coverprofile=cover.out
ok  	./calc	0.018s	coverage: 100.0% of statements
$ go tool cover -html=cover.out -o cover.html

cover.html 中,你會看到 if b == 0 這行被 綠色 標示,代表已被測試覆蓋。

3. 使用 -covermode=count 分析執行次數

$ go test ./calc -covermode=count -coverprofile=count.out
ok  	./calc	0.020s	coverage: 100.0% of statements
$ go tool cover -func=count.out
calc/add.go:8:	Add	100.0%
calc/divide.go:8:	Divide	100.0%
total:							(statements)	100.0%

若你在測試中多次呼叫 Addcount 模式會顯示每行的執行次數,協助找出「熱點」或是測試不夠分散的情況。

4. 結合所有套件的全域覆蓋率

假設專案結構如下:

myproj/
 ├─ cmd/
 │   └─ main.go
 ├─ internal/
 │   └─ service/
 │       └─ service.go
 └─ pkg/
     └─ util/
         └─ util.go

要一次測量所有套件:

$ go test ./... -covermode=atomic -coverprofile=all.out
ok  	myproj/cmd	0.030s	coverage: 85.7% of statements
ok  	myproj/internal/service	0.045s	coverage: 92.3% of statements
ok  	myproj/pkg/util	0.020s	coverage: 78.9% of statements

接著產生 HTML:

$ go tool cover -html=all.out -o coverage.html

5. 在 CI 中設定覆蓋率門檻

以 GitHub Actions 為例:

name: Go CI

on:
  push:
    branches: [ main ]

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 with coverage
        run: |
          go test ./... -covermode=atomic -coverprofile=coverage.out
          go tool cover -func=coverage.out
      - name: Enforce coverage threshold
        run: |
          go tool cover -func=coverage.out | grep total | awk '{print $3}' | cut -d. -f1 > COVERAGE
          COVERAGE=$(cat COVERAGE)
          if [ "$COVERAGE" -lt 80 ]; then
            echo "Coverage $COVERAGE% is below the 80% threshold"
            exit 1
          fi

這段腳本會在每次推送時檢查總覆蓋率是否達到 80%,未達標則失敗。


常見陷阱與最佳實踐

陷阱 說明 解決方案
只看總體百分比 高總體覆蓋率可能掩蓋關鍵路徑未被測試的事實。 針對 關鍵業務邏輯(如錯誤處理、邊界值)設定 局部 覆蓋目標。
測試依賴外部資源 若測試需要資料庫或網路,覆蓋率可能因為 skip 而下降。 使用 mockfakein‑memory 替代品,確保測試能在純程式碼層面執行。
忽略 -covermode=count 只看是否執行過,無法判斷測試是否足夠分散。 在大型專案使用 count,找出被過度測試或完全未測試的程式區段。
測試程式碼本身未被覆蓋 測試檔案如果缺乏分支,會降低整體覆蓋率。 為測試程式碼加入 子測試 (t.Run) 或 表格驅動測試,提升測試檔案的覆蓋度。
CI 中忘記 -coverpkg 僅測試當前套件,導致跨套件的公共函式未被計算。 使用 -coverpkg=./... 或明確列出需要測量的套件。

最佳實踐

  1. 設定合理門檻:新專案可先設 70% → 90%,避免過度追求 100% 而浪費時間。
  2. 結合靜態分析:使用 golangci-lintdeadcodeunused 檢查,減少無用程式碼。
  3. 表格驅動測試:一次測試多組輸入,讓分支更完整。
  4. 持續檢視 HTML 報表:每次 CI 失敗時,直接在 PR 中附上 coverage.html,讓開發者快速定位未覆蓋區塊。
  5. 與代碼審查結合:在 PR 評審時,檢查「新增程式碼」的覆蓋率變化,確保新功能不降低整體品質。

實際應用場景

場景 為何需要覆蓋率 實作要點
微服務 API 每個端點的參數驗證、錯誤回傳都必須被測試。 為每個 handler 寫表格測試,使用 httptest,產生 coverage.out,在 CI 中設定 85% 以上。
資料處理管線 大量資料轉換容易產生隱藏的邏輯缺陷。 使用 count 模式找出被頻繁執行的路徑,針對極端資料(空值、極大值)補足測試。
嵌入式/IoT 程式 硬體抽象層的錯誤往往難以重現。 透過介面抽象與 mock,讓硬體相關程式碼也能被 go test 覆蓋,並以 atomic 模式保證並行測試的正確性。
金融風險計算 法規要求必須證明核心演算法的正確性。 為每個風險模型寫完整的單元測試,並在 CI 中設定 100% 的覆蓋率門檻,配合 go vetstaticcheck 形成雙重保證。

總結

覆蓋率測試是 量化測試品質 的第一道防線。Go 提供的 -cover 系列指令讓我們能輕鬆產生統計、圖形與執行次數的資訊,配合 CI/CD 可以把測試門檻寫進自動化流程。要避免只追求數字的陷阱,必須:

  • 聚焦關鍵路徑,而非僅看總體百分比。
  • 使用多種模式setcountatomic)來檢視不同層面的覆蓋情況。
  • 結合 Mock、表格測試與靜態分析,提升測試的深度與廣度。

只要遵循上述最佳實踐,從 小型套件大型微服務,都能在開發過程中即時掌握測試盲點,降低缺陷流入正式環境的風險。祝你在 Golang 的測試旅程中,寫出更可靠、更可維護的程式碼!