Python 自動化測試最佳實踐
📅 技術深度指南|關鍵字:Python・自動化測試・pytest・單元測試
在現代軟體開發流程中,自動化測試已不再是可選項,而是保障程式碼品質的核心基石。隨著專案規模擴大,手動測試不僅耗時,更容易遺漏邊緣情境(Edge Case)。Python 憑藉其簡潔的語法與豐富的測試生態系,成為自動化測試的熱門首選。
本篇文章將深入介紹如何運用 pytest 框架建立高品質的單元測試,並分享業界驗證過的最佳實踐,幫助你打造可靠、易維護的測試套件。無論你是測試新手或資深工程師,都能從中獲得實用的建議。
🧪 為什麼選擇 pytest?
Python 內建的 unittest 模組雖然功能完整,但語法冗長、設定繁瑣。相較之下,pytest 以「零樣板」的設計哲學著稱,讓開發者用最少的程式碼撰寫出表達力強的測試案例。它同時向下相容 unittest,遷移成本極低。
pytest 的核心優勢在於其強大的 Fixture 機制、豐富的插件生態(如 pytest-cov、pytest-mock),以及清晰直觀的錯誤訊息輸出。這些特性共同構成了一個高效的測試開發體驗,大幅縮短 Debug 時間。
# 安裝 pytest 與常用插件
pip install pytest pytest-cov pytest-mock
# 執行所有測試
pytest
# 產生覆蓋率報告
pytest --cov=src --cov-report=html
# 只執行特定標籤的測試
pytest -m "unit"
🏗️ 測試結構與命名規範
良好的測試結構是可維護性的根本。業界廣泛採用 AAA 模式(Arrange、Act、Assert)來組織每個測試案例:先準備測試環境與資料,再執行被測行為,最後驗證結果是否符合預期。這種結構讓測試意圖一目了然。
在命名上,測試函式應以 test_ 為前綴,並使用描述性名稱清楚說明「測試情境」與「預期結果」。例如 test_calculate_discount_when_vip_user_returns_20_percent,讓閱讀者無需查看實作就能理解測試目的。
# src/discount.py - 被測試的目標模組
class DiscountService:
def calculate_discount(self, user_type: str, amount: float) -> float:
"""計算折扣後金額"""
if user_type == "vip":
return amount * 0.8
elif user_type == "member":
return amount * 0.9
return amount
# tests/test_discount.py - 對應的測試檔案
import pytest
from src.discount import DiscountService
class TestDiscountService:
def setup_method(self):
"""每個測試前自動執行:初始化服務實例"""
self.service = DiscountService()
def test_calculate_discount_when_vip_user_returns_20_percent(self):
# Arrange(準備)
user_type = "vip"
original_amount = 1000.0
# Act(執行)
result = self.service.calculate_discount(user_type, original_amount)
# Assert(驗證)
assert result == 800.0, f"VIP 折扣後應為 800,實際為 {result}"
def test_calculate_discount_when_regular_user_returns_full_price(self):
# Arrange
user_type = "guest"
original_amount = 500.0
# Act
result = self.service.calculate_discount(user_type, original_amount)
# Assert
assert result == 500.0
⚙️ Fixture 與參數化測試
Fixture 是 pytest 最強大的功能之一,它以依賴注入的方式管理測試資源(如資料庫連線、假資料、Mock 物件),並透過 scope 參數控制資源的生命週期(function / class / module / session),有效避免重複初始化的效能損耗。
參數化測試(Parametrize)則讓你用單一測試函式覆蓋多組輸入與預期輸出,避免撰寫大量重複的測試案例。這不僅提升程式碼的整潔度,更確保邊界值(Boundary Value)都被完整驗證。
import pytest
from unittest.mock import MagicMock
from src.discount import DiscountService
# ── Fixture 範例 ──────────────────────────────────────────
@pytest.fixture(scope="module")
def discount_service():
"""模組層級 Fixture:整個測試模組共用同一個服務實例"""
print("\n⚙️ 初始化 DiscountService")
service = DiscountService()
yield service
print("\n🧹 清理 DiscountService 資源")
@pytest.fixture
def mock_database():
"""函式層級 Fixture:每個測試獨立的 Mock 資料庫"""
db = MagicMock()
db.get_user.return_value = {"id": 1, "type": "vip"}
return db
# ── 參數化測試範例 ─────────────────────────────────────────
@pytest.mark.parametrize("user_type, amount, expected", [
("vip", 1000.0, 800.0), # VIP 享 8 折
("member", 500.0, 450.0), # 會員享 9 折
("guest", 300.0, 300.0), # 訪客無折扣
("vip", 0.0, 0.0), # 邊界值:金額為零
])
def test_calculate_discount_parametrized(
discount_service, # 注入 Fixture
user_type, amount, expected
):
result = discount_service.calculate_discount(user_type, amount)
assert result == pytest.approx(expected, rel=1e-6)
# ── 例外測試範例 ───────────────────────────────────────────
def test_calculate_discount_raises_on_negative_amount(discount_service):
with pytest.raises(ValueError, match="金額不得為負數"):
discount_service.calculate_discount("vip", -100.0)
📁 專案結構與 CI/CD 整合
建議將測試程式碼與主程式碼明確分離,採用 src-layout 的目錄結構,並在根目錄放置 pytest.ini 或 pyproject.toml 統一管理測試設定。清晰的目錄結構讓新成員能快速上手,也方便 CI/CD 工具自動定位測試檔案。
將 pytest 整合進 GitHub Actions 或 GitLab CI,可在每次 Pull Request 時自動執行測試並產生覆蓋率報告。設定覆蓋率門檻(如最低 80%),能有效防止未測試的程式碼被合入主分支,形成可靠的品質防護網。
# 推薦的專案目錄結構
my_project/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── discount.py
│ └── user.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # 全域 Fixture 定義
│ ├── unit/
│ │ └── test_discount.py
│ └── integration/
│ └── test_order_flow.py
├── pytest.ini # pytest 設定檔
└── pyproject.toml
# pytest.ini 設定範例
[pytest]
testpaths = tests
addopts = -v --cov=src --cov-report=term-missing --cov-fail-under=80
markers =
unit: 單元測試
integration: 整合測試
slow: 執行時間較長的測試
# .github/workflows/test.yml(GitHub Actions)
name: Python 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
💡 Python 自動化測試最佳實踐重點整理
- 選用 pytest:語法簡潔、插件豐富,是 Python 測試的業界標準
- 遵循 AAA 模式:Arrange → Act → Assert,讓測試結構清晰易讀
- 善用描述性命名:測試名稱即文件,清楚表達情境與預期行為
- 活用 Fixture:透過 scope 控制資源生命週期,避免重複初始化
- 參數化測試:一個測試涵蓋多種輸入,不遺漏邊界值與異常情境
- 維持高覆蓋率:建議設定 80% 以上門檻,但覆蓋率不等於品質
- 整合 CI/CD:讓每次提交自動觸發測試,形成持續的品質防護
- 分層測試策略:區分 unit / integration / e2e,分別管理執行成本