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






