python-testing-patterns

python-testing-patterns

熱門

使用 pytest、fixtures、mocking 與測試驅動開發,實作全面的測試策略。適用於撰寫 Python 測試、建立測試套件或實作測試最佳實務。

3.8萬星標
4094分支
更新於 2026/7/22
SKILL.md
readonlyread-only
name
python-testing-patterns
description

使用 pytest、fixtures、mocking 與測試驅動開發,實作全面的測試策略。適用於撰寫 Python 測試、建立測試套件或實作測試最佳實務。

Python 測試模式

使用 pytest、fixtures、mocking、參數化與測試驅動開發實作穩健測試策略的完整指南。

何時使用此技能

  • 為 Python 程式碼撰寫單元測試
  • 建立測試套件與測試基礎設施
  • 實作測試驅動開發 (TDD)
  • 為 API 與服務建立整合測試
  • Mock 外部相依與服務
  • 測試非同步程式碼與並行操作
  • 在 CI/CD 中設定持續測試
  • 實作基於屬性的測試
  • 測試資料庫操作
  • 除錯失敗的測試

核心概念

1. 測試類型

  • 單元測試:隔離測試個別函式/類別
  • 整合測試:測試元件間的互動
  • 功能測試:端到端測試完整功能
  • 效能測試:測量速度與資源使用

2. 測試結構 (AAA 模式)

  • Arrange:設定測試資料與前置條件
  • Act:執行受測程式碼
  • Assert:驗證結果

3. 測試覆蓋率

  • 衡量哪些程式碼被測試執行到
  • 找出未測試的程式碼路徑
  • 目標是有意義的覆蓋率,而非只是高百分比

4. 測試隔離

  • 測試應彼此獨立
  • 測試之間不共享狀態
  • 每個測試應自行清理

快速開始

# test_example.py
def add(a, b):
    return a + b

def test_add():
    """基本測試範例。"""
    result = add(2, 3)
    assert result == 5

def test_add_negative():
    """測試負數。"""
    assert add(-1, 1) == 0

# 執行:pytest test_example.py

詳細模式與實作範例

詳細模式文件位於 references/details.md。當上述導覽層級不足時,請閱讀該檔案。

測試最佳實務

測試組織

# tests/
#   __init__.py
#   conftest.py           # 共享 fixtures
#   test_unit/            # 單元測試
#     test_models.py
#     test_utils.py
#   test_integration/     # 整合測試
#     test_api.py
#     test_database.py
#   test_e2e/            # 端到端測試
#     test_workflows.py

測試命名慣例

常見模式:test_<unit>_<scenario>_<expected_outcome>。可依團隊偏好調整。

# 模式:test_<unit>_<scenario>_<expected>
def test_create_user_with_valid_data_returns_user():
    ...

def test_create_user_with_duplicate_email_raises_conflict():
    ...

def test_get_user_with_unknown_id_returns_none():
    ...

# 好的測試名稱 - 清晰且具描述性
def test_user_creation_with_valid_data():
    """清楚的名稱描述測試內容。"""
    pass

def test_login_fails_with_invalid_password():
    """名稱描述預期行為。"""
    pass

def test_api_returns_404_for_missing_resource():
    """具體說明輸入與預期結果。"""
    pass

# 不好的測試名稱 - 應避免
def test_1():  # 不具描述性
    pass

def test_user():  # 太模糊
    pass

def test_function():  # 未說明測試內容
    pass

測試重試行為

使用 mock side effect 驗證重試邏輯是否正確運作。

from unittest.mock import Mock

def test_retries_on_transient_error():
    """測試服務在暫時性失敗時會重試。"""
    client = Mock()
    # 失敗兩次,然後成功
    client.request.side_effect = [
        ConnectionError("Failed"),
        ConnectionError("Failed"),
        {"status": "ok"},
    ]

    service = ServiceWithRetry(client, max_retries=3)
    result = service.fetch()

    assert result == {"status": "ok"}
    assert client.request.call_count == 3

def test_gives_up_after_max_retries():
    """測試服務在達到最大重試次數後停止重試。"""
    client = Mock()
    client.request.side_effect = ConnectionError("Failed")

    service = ServiceWithRetry(client, max_retries=3)

    with pytest.raises(ConnectionError):
        service.fetch()

    assert client.request.call_count == 3

def test_does_not_retry_on_permanent_error():
    """測試永久性錯誤不會重試。"""
    client = Mock()
    client.request.side_effect = ValueError("Invalid input")

    service = ServiceWithRetry(client, max_retries=3)

    with pytest.raises(ValueError):
        service.fetch()

    # 只呼叫一次 - ValueError 不重試
    assert client.request.call_count == 1

使用 Freezegun Mock 時間

使用 freezegun 控制測試中的時間,以獲得可預測的時序行為。

from freezegun import freeze_time
from datetime import datetime, timedelta

@freeze_time("2026-01-15 10:00:00")
def test_token_expiry():
    """測試 token 在正確時間過期。"""
    token = create_token(expires_in_seconds=3600)
    assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)

@freeze_time("2026-01-15 10:00:00")
def test_is_expired_returns_false_before_expiry():
    """測試 token 在有效期內未過期。"""
    token = create_token(expires_in_seconds=3600)
    assert not token.is_expired()

@freeze_time("2026-01-15 12:00:00")
def test_is_expired_returns_true_after_expiry():
    """測試 token 在有效期後已過期。"""
    token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))
    assert token.is_expired()

def test_with_time_travel():
    """使用 freeze_time context 測試跨時間的行為。"""
    with freeze_time("2026-01-01") as frozen_time:
        item = create_item()
        assert item.created_at == datetime(2026, 1, 1)

        # 向前移動時間
        frozen_time.move_to("2026-01-15")
        assert item.age_days == 14

測試標記

# test_markers.py
import pytest

@pytest.mark.slow
def test_slow_operation():
    """標記慢速測試。"""
    import time
    time.sleep(2)


@pytest.mark.integration
def test_database_integration():
    """標記整合測試。"""
    pass


@pytest.mark.skip(reason="功能尚未實作")
def test_future_feature():
    """暫時跳過測試。"""
    pass


@pytest.mark.skipif(os.name == "nt", reason="僅限 Unix 測試")
def test_unix_specific():
    """條件式跳過。"""
    pass


@pytest.mark.xfail(reason="已知錯誤 #123")
def test_known_bug():
    """標記預期失敗。"""
    assert False


# 執行方式:
# pytest -m slow          # 只執行慢速測試
# pytest -m "not slow"    # 跳過慢速測試
# pytest -m integration   # 執行整合測試

覆蓋率報告

# 安裝 coverage
pip install pytest-cov

# 執行測試並產生覆蓋率
pytest --cov=myapp tests/

# 產生 HTML 報告
pytest --cov=myapp --cov-report=html tests/

# 若覆蓋率低於門檻則失敗
pytest --cov=myapp --cov-fail-under=80 tests/

# 顯示遺漏行
pytest --cov=myapp --cov-report=term-missing tests/

進階模式(非同步測試、monkeypatching、基於屬性的測試、資料庫測試、CI/CD 整合與設定)請參閱 references/advanced-patterns.md