iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Software Development

Python pytest TDD 實戰:從零開始的測試驅動開發系列 第 10

Day 09 - 測試覆蓋率:你的測試真的夠完整嗎? 📊

  • 分享至 

  • xImage
  •  

今天的目標

還記得昨天我們學會了例外處理測試,確保程式在錯誤情況下的穩定運行嗎?今天要面對一個更深層的問題:「我們的測試到底覆蓋了多少程式碼?」

測試覆蓋率(Test Coverage)就像是程式碼的健康檢查報告,告訴你哪些程式碼被測試了,哪些還沒有。它不是萬能的,但是一個非常有用的指標,幫助我們找出測試的盲點。

這週的學習地圖

我們正在第九天的基礎測試概念學習:

基礎測試概念(第 1-10 天)
├── ✅ Day 1: 環境設置與第一個測試
├── ✅ Day 2: 測試斷言
├── ✅ Day 3: TDD 紅綠重構
├── ✅ Day 4: 測試結構
├── ✅ Day 5: 生命週期
├── ✅ Day 6: 參數化測試
├── ✅ Day 7: 測試替身基礎
├── ✅ Day 8: 例外處理測試
├── 📍 Day 9: 測試覆蓋率(今天)
└── Day 10: 重構技巧

學習目標

今天結束後,你將學會:

  • 理解測試覆蓋率的概念和重要性
  • 掌握 pytest-cov 的覆蓋率工具使用
  • 學會分析覆蓋率報告
  • 理解不同類型的覆蓋率指標

📊 測試覆蓋率讓我們能夠量化測試完整性,發現潛在的測試盲點。

為什麼需要測試覆蓋率? 🎯

覆蓋率幫助我們:

  • 發現盲點:找出未測試的程式碼
  • 提升品質:確保關鍵路徑都有測試
  • 重構信心:更安全地進行重構
  • 團隊溝通:提供客觀的測試指標

Python pytest-cov 覆蓋率設置

基本配置

更新 requirements-dev.txt

pytest==7.4.0
pytest-cov==4.1.0

安裝依賴:

pip install -r requirements-dev.txt

配置 pyproject.toml

[tool.coverage.run]
source = ["src"]
branch = true
omit = [
    "*/tests/*",
    "*/venv/*"
]

[tool.coverage.report]
show_missing = true
skip_empty = true
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise NotImplementedError"
]

[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=term-missing --cov-fail-under=80"
testpaths = ["tests"]

覆蓋率類型詳解 📈

四大覆蓋率指標

  1. 語句覆蓋率:執行了多少程式碼語句(如 80/100 = 80%)
  2. 分支覆蓋率:測試了多少條件分支(if/else、switch)
  3. 函數覆蓋率:調用了多少函數,找出「死代碼」
  4. 行覆蓋率:執行了多少行程式碼

實際範例:計算器類別

讓我們用一個實際的例子來理解覆蓋率的重要性。

建立 src/day09/calculator.py

class Calculator:
    def divide(self, a: float, b: float) -> float:
        if b == 0:
            raise ValueError('Cannot divide by zero')
        return a / b
    
    def get_discount(self, amount: float, is_vip: bool = False) -> float:
        if is_vip:
            return amount * 0.2
        return amount * 0.1 if amount > 100 else 0

建立 tests/day09/test_coverage_demo.py

import pytest
from src.day09.calculator import Calculator

def test_divides_numbers_correctly():
    calculator = Calculator()
    assert calculator.divide(10.0, 2.0) == 5.0

def test_throws_exception_for_division_by_zero():
    calculator = Calculator()
    with pytest.raises(ValueError):
        calculator.divide(10.0, 0.0)

def test_calculates_vip_discount():
    calculator = Calculator()
    assert calculator.get_discount(100.0, True) == 20.0

def test_calculates_regular_discount_for_large_amount():
    calculator = Calculator()
    assert calculator.get_discount(150.0, False) == 15.0

def test_returns_zero_discount_for_small_amount():
    calculator = Calculator()
    assert calculator.get_discount(50.0, False) == 0.0

執行覆蓋率報告

基本命令

# 產生覆蓋率報告
pytest --cov=src tests/day09/

# 產生 HTML 報告
pytest --cov=src --cov-report=html

覆蓋率分析

執行後會看到:

Tests:    5 passed
Coverage: 85.7% (lines), 100% (functions), 75% (branches)

Uncovered lines:
- calculator.py:15 (處理負數情況)
- calculator.py:20 (邊界條件)

改善覆蓋率策略

1. 分析覆蓋率報告

覆蓋率報告標示:

  • 綠色:已測試的程式碼
  • 紅色:未測試的程式碼
  • ⚠️ 黃色:部分分支未測試

2. 改善流程

# 產生詳細報告
pytest --cov=src --cov-report=html
open htmlcov/index.html

優先改善:核心業務邏輯、錯誤處理、複雜分支

# 補強未覆蓋的邊界條件
def test_handles_edge_cases():
    calculator = Calculator()
    
    # 測試零值情況
    assert calculator.get_discount(0.0, True) == 0.0
    
    # 測試負值情況  
    assert calculator.get_discount(-10.0, False) == 0.0
    
    # 測試極大值情況(使用容差比較避免浮點數精度問題)
    assert abs(calculator.get_discount(999999.0, True) - 199999.8) < 0.01

3. 覆蓋率最佳實踐

# ❌ 為了覆蓋率寫無意義測試
def test_covers_getter():
    assert MyClass().get_value() is None

# ✅ 寫有價值的測試
def test_calculates_discount_correctly():
    calculator = Calculator()
    assert calculator.get_discount(100.0, True) == 20.0
    assert calculator.get_discount(50.0, False) == 0.0
    assert calculator.get_discount(200.0, False) == 20.0

避免:為數字而測試、忽略實際價值、測試實作細節

實用技巧與最佳實踐

覆蓋率門檻建議 📈

  • 新專案:70-80%
  • 關鍵系統:85-90%
  • 金融/醫療:90-95%
  • 最佳平衡:85-90%

CI/CD 整合

# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - run: pip install -r requirements-dev.txt
      - run: pytest --cov=src --cov-fail-under=80

今天學到什麼?

透過今天的學習,我們掌握了:

  1. 測試覆蓋率的概念:了解語句、分支、函數、行覆蓋率的差異
  2. pytest-cov 覆蓋率工具:配置和使用多種覆蓋率報告格式
  3. 覆蓋率分析技巧:識別測試盲點和改善策略
  4. 最佳實踐:平衡覆蓋率和測試品質,避免為覆蓋率而測試
  5. 實戰應用:處理複雜的分支邏輯覆蓋

測試覆蓋率讓我們能夠客觀衡量測試完整性,發現未測試的程式碼路徑,指導測試策略。記住,覆蓋率是品質的指標之一,但不是唯一指標!

重點回顧 📝

今天的關鍵要點:

  1. 覆蓋率類型

    • 語句覆蓋率:執行了多少語句
    • 分支覆蓋率:測試了多少條件路徑
    • 函數覆蓋率:調用了多少函數
    • 行覆蓋率:執行了多少行
  2. 工具配置

    • 使用 pytest-cov
    • 設定合理的覆蓋率閾值
    • 產生多種報告格式
  3. 最佳實踐

    • 80-90% 是合理目標
    • 關注業務邏輯測試
    • 避免無意義的測試

明天預告 🔮

明天是第十天,我們將學習重構技巧,了解如何在測試的保護下安全地改善程式碼結構。你會學到如何運用測試來支援重構,以及常見的重構模式。

總結

今天我們深入探討了測試覆蓋率,這是 TDD 過程中重要的品質指標。我們學會了如何配置和使用 pytest-cov 的覆蓋率工具,理解了不同類型的覆蓋率指標,並知道如何合理地運用覆蓋率來改善測試品質。

記住:覆蓋率是手段而非目的。用覆蓋率來指導測試策略,而不是盲目追求數字!明天我們將學習如何在測試的保護下進行重構。 🚀


挑戰作業:檢查你現有專案的測試覆蓋率,找出至少三個未被測試的函數,為它們補充測試案例。


上一篇
Day 09 - 測試覆蓋率:你的測試真的夠完整嗎? 📊
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言