本文 AI 產出,尚未審核

Python 單元測試與除錯:測試覆蓋率(Coverage)

簡介

在軟體開發的整個生命週期中,測試是確保程式品質的關鍵環節。即使寫了完整的單元測試,若不清楚測試到底涵蓋了多少程式碼,仍可能留下隱藏的缺陷。這時 測試覆蓋率(coverage)就派上用場:它量化了測試執行時實際觸及的程式碼比例,讓開發者可以直觀地看到哪些路徑尚未被測試。

對初學者而言,了解覆蓋率的概念與工具使用,能養成「測試驅動」的好習慣;對中級開發者來說,則是提升測試品質、減少回歸錯誤的必備技巧。本文將從概念說明、實作範例、常見陷阱到最佳實踐,全面介紹 Python 中的測試覆蓋率。


核心概念

什麼是測試覆蓋率?

測試覆蓋率是指在執行測試套件時,程式碼中被執行(或「走過」)的行數、分支、條件等的比例。常見的衡量指標包括:

指標 說明
Line Coverage 被執行的程式碼行數佔總行數的百分比
Branch Coverage 每個 if/elsetry/except 等分支是否皆被測試
Statement Coverage 每條語句是否被執行
Function/Method Coverage 每個函式或方法是否被呼叫

為什麼要追蹤覆蓋率?

  1. 發現盲點:自動化測試往往只測試「主要路徑」,忽略邊緣情況。覆蓋率報告能一目了然地指出未被觸及的程式碼。
  2. 提升信心:在持續整合(CI)流程中加入覆蓋率門檻,可確保每次提交都有基本的測試保護。
  3. 指導測試設計:根據報告調整測試案例,避免過度或不足的測試。

常用工具:coverage.py

Python 官方推薦的測試覆蓋率工具是 coverage.py。它支援多種執行方式(直接跑腳本、與 pytest 整合、在 CI 中使用),且能產生 HTML、XML、JSON 等多種報表。

安裝方式非常簡單:

pip install coverage

程式碼範例

以下示範如何在不同情境下使用 coverage.py,並說明常見的測試技巧。

範例 1:基本使用 – 產生簡易報表

# file: calculator.py
def add(a, b):
    """回傳兩數相加的結果"""
    return a + b

def divide(a, b):
    """除法,b 為 0 時拋出例外"""
    if b == 0:
        raise ValueError("除數不能為 0")
    return a / b
# file: test_calculator.py
import pytest
from calculator import add, divide

def test_add():
    assert add(2, 3) == 5

def test_divide_normal():
    assert divide(10, 2) == 5

def test_divide_zero():
    with pytest.raises(ValueError):
        divide(10, 0)

執行測試並收集覆蓋率:

coverage run -m pytest
coverage report -m   # 顯示行覆蓋率

輸出示例:

Name               Stmts   Miss  Cover
--------------------------------------
calculator.py          9      0   100%
test_calculator.py    15      0   100%

重點-m 參數讓 coverage 直接呼叫 pytest,不需要額外的設定檔。


範例 2:產生 HTML 報表,視覺化未覆蓋程式碼

coverage html          # 產生 htmlcov 目錄

在瀏覽器開啟 htmlcov/index.html,即可看到每個檔案的彩色標示:

  • 綠色:已執行
  • 紅色:未執行
  • 黃色:部分執行(例如只有 if 的一側被測)

透過這種視覺化,開發者可以快速定位「死碼」或缺乏測試的分支。


範例 3:與 pytest-cov 整合,設定最低覆蓋率門檻

pip install pytest-cov

pytest 執行時直接加入 --cov 參數:

pytest --cov=calculator --cov-fail-under=90
  • --cov=calculator:只統計 calculator 套件的覆蓋率
  • --cov-fail-under=90:若覆蓋率低於 90% 會讓測試失敗,適合 CI 使用

範例 4:測試多分支程式 – Branch Coverage

# file: utils.py
def classify_age(age):
    """根據年齡分類"""
    if age < 0:
        raise ValueError("年齡不可為負")
    if age < 13:
        return "兒童"
    elif age < 20:
        return "青少年"
    elif age < 65:
        return "成人"
    else:
        return "長者"
# file: test_utils.py
import pytest
from utils import classify_age

def test_negative():
    with pytest.raises(ValueError):
        classify_age(-1)

@pytest.mark.parametrize("age,expected", [
    (5, "兒童"),
    (16, "青少年"),
    (30, "成人"),
    (70, "長者"),
])
def test_classify(age, expected):
    assert classify_age(age) == expected

使用 coverage--branch 參數來檢查分支覆蓋率:

coverage run --branch -m pytest
coverage report -m

報表會顯示每行的 branch 數字,例如 4 2 50% 表示第 4 行有兩個分支,其中只有 50% 被測。


範例 5:排除不需要測試的程式碼(如 __init__.py、設定檔)

在專案根目錄建立 .coveragerc

# .coveragerc
[run]
omit =
    */tests/*
    */__init__.py
    setup.py

執行 coverage run -m pytest 時,coverage 會自動忽略上述檔案,讓報表更聚焦於業務邏輯。


常見陷阱與最佳實踐

陷阱 說明 最佳實踐
只看總覆蓋率 總覆蓋率高(如 95%)不代表所有重要分支都被測。 針對 branchcondition 也設定門檻,尤其是錯誤處理路徑。
忽略例外情況 測試往往只驗證「正常」流程,導致 except 分支未被執行。 使用 pytest.raisesparameterize 測試各種例外。
過度依賴覆蓋率 100% 覆蓋率不代表測試品質好,仍可能缺乏斷言或測試資料不完整。 確保每個測試都有 assert,且涵蓋不同的輸入組合。
未排除第三方套件 直接測量整個環境會把外部套件的程式碼算進覆蓋率,結果失真。 .coveragerc 中使用 source=omit= 只測量自家模組。
忽略 CI 整合 手動執行覆蓋率報表不會自動阻止錯誤併入主幹。 在 GitHub Actions、GitLab CI 等加入 coverage 步驟,並設定失敗門檻。

最佳實踐總結

  1. 從一開始就加入測試,避免後期大幅補測。
  2. 使用 pytest-cov,在開發階段即看到即時覆蓋率。
  3. 設定合理門檻(如 line ≥ 80%、branch ≥ 70%),並隨專案成熟度逐步提升。
  4. 排除非業務程式碼,讓報表聚焦於真正需要保護的邏輯。
  5. 持續監控:在 CI 中產生 HTML 報表或使用 codecov.iocoveralls.io 讓團隊即時看到趨勢。

實際應用場景

  1. 新功能開發:在實作新函式前,先寫測試案例;完成後跑 coverage 確認所有分支都有測試。
  2. Bug 修復:根據缺陷回報,寫出能觸發錯誤的測試,確保未來不會再次出現同樣問題。
  3. 遺留系統重構:先以 coverage 為基礎,找出未被測的老舊程式碼,逐步為其補上測試,再進行重構。
  4. CI/CD 流程:在 Pull Request 檢查階段加入 coverage 步驟,若未達門檻即阻止合併,提升主幹品質。
  5. 安全敏感模組:對加密、權限檢查等關鍵模組設定更高的分支覆蓋率門檻(如 90%+),降低安全漏洞風險。

總結

測試覆蓋率是 量化測試完整性 的有效工具,配合 coverage.pypytest-cov 等生態系統,開發者可以輕鬆掌握程式碼被測程度。

  • 了解 LineBranchFunction 等指標的差異,才能針對不同需求設定合適的門檻。
  • 透過 HTML 報表CI 整合.coveragerc 排除不相關檔案,讓報表更具可讀性。
  • 警惕「只看總數」的陷阱,強調 斷言例外測試多分支覆蓋,才能真正提升程式品質。

把測試覆蓋率當作每日開發的「健康檢查」儀表板,長期下來不僅能減少回歸錯誤,還能培養團隊的測試文化,讓 Python 專案更穩健、更易維護。祝你在測試之路上玩得開心,寫出更可靠的程式!