還記得昨天我們學會了例外處理測試,確保程式在錯誤情況下的穩定運行嗎?今天要面對一個更深層的問題:「我們的測試到底覆蓋了多少程式碼?」
測試覆蓋率(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: 重構技巧
今天結束後,你將學會:
📊 測試覆蓋率讓我們能夠量化測試完整性,發現潛在的測試盲點。
覆蓋率幫助我們:
更新 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"]
讓我們用一個實際的例子來理解覆蓋率的重要性。
建立 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 (邊界條件)
覆蓋率報告標示:
# 產生詳細報告
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
# ❌ 為了覆蓋率寫無意義測試
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
避免:為數字而測試、忽略實際價值、測試實作細節
# .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
透過今天的學習,我們掌握了:
測試覆蓋率讓我們能夠客觀衡量測試完整性,發現未測試的程式碼路徑,指導測試策略。記住,覆蓋率是品質的指標之一,但不是唯一指標!
今天的關鍵要點:
覆蓋率類型
工具配置
最佳實踐
明天是第十天,我們將學習重構技巧,了解如何在測試的保護下安全地改善程式碼結構。你會學到如何運用測試來支援重構,以及常見的重構模式。
今天我們深入探討了測試覆蓋率,這是 TDD 過程中重要的品質指標。我們學會了如何配置和使用 pytest-cov 的覆蓋率工具,理解了不同類型的覆蓋率指標,並知道如何合理地運用覆蓋率來改善測試品質。
記住:覆蓋率是手段而非目的。用覆蓋率來指導測試策略,而不是盲目追求數字!明天我們將學習如何在測試的保護下進行重構。 🚀
挑戰作業:檢查你現有專案的測試覆蓋率,找出至少三個未被測試的函數,為它們補充測試案例。