跳到主要內容

Python 自動化測試完全指南:從入門到精通

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.inipyproject.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,分別管理執行成本