「為什麼我的測試總是跑不過?明明程式碼都沒問題啊!」經過一番調查,發現是因為他的測試環境設置不一致。這讓我想到,如果能在測試的關鍵時刻自動執行一些設置或清理工作,是不是就能避免這種問題了?
今天我們要學習 pytest 的測試生命週期 Hook 功能,確保測試環境的一致性!
基礎篇 Kata 篇 框架篇
Day 1-10 Day 11-17 Day 18-24 ← 我們在這裡!
[=====] [=====] [=====>]
測試生命週期管理
├── 🔄 pytest_runtest_setup/teardown
├── 📦 pytest_sessionstart/finish
├── ⚡ 效能監控 Hook
└── 🎯 全域 Hook 配置
測試生命週期 Hook 是在測試執行的特定時機點自動觸發的程式碼。它們幫助我們管理測試環境,確保每個測試都在相同的條件下執行。
pytest 提供了生命週期 Hook 來管理測試環境。
# 建立 conftest.py
import time
import pytest
from typing import Any
def pytest_runtest_setup(item: Any) -> None:
"""每個測試執行前的 Hook"""
print(f"\n🚀 開始測試: {item.name}")
item.start_time = time.time()
def pytest_runtest_teardown(item: Any) -> None:
"""每個測試執行後的 Hook"""
duration = time.time() - getattr(item, 'start_time', 0)
print(f"⏱️ 執行時間: {duration:.3f} 秒")
# 建立 tests/day24/test_hook_lifecycle.py
def test_demonstrate_hooks():
"""展示 Hook 執行順序"""
print("執行測試中...")
assert True
# 更新 conftest.py
def pytest_sessionstart(session):
"""測試會話開始"""
print("\n📦 測試會話開始")
def pytest_sessionfinish(session, exitstatus):
"""測試會話結束"""
print(f"\n✅ 測試完成,狀態碼: {exitstatus}")
def pytest_runtest_makereport(item, call):
"""產生測試報告"""
if call.when == "call":
outcome = "通過" if call.excinfo is None else "失敗"
print(f"📊 測試 {item.name} - {outcome}")
# 建立 tests/day24/performance_plugin.py
from typing import Dict, List
class PerformanceMonitor:
"""效能監控器"""
def __init__(self):
self.results: Dict[str, float] = {}
self.slow_tests: List[tuple] = []
self.threshold = 1.0 # 慢測試閾值(秒)
def record_duration(self, test_name: str, duration: float):
"""記錄測試執行時間"""
self.results[test_name] = duration
if duration > self.threshold:
self.slow_tests.append((test_name, duration))
def generate_report(self) -> str:
"""產生效能報告"""
if not self.results:
return "沒有測試執行"
avg_time = sum(self.results.values()) / len(self.results)
report = [
"\n" + "="*50,
f"📊 總測試數: {len(self.results)}",
f"⏱️ 平均時間: {avg_time:.3f} 秒",
]
if self.slow_tests:
report.append("\n⚠️ 慢速測試:")
for name, duration in self.slow_tests:
report.append(f" - {name}: {duration:.3f} 秒")
return "\n".join(report)
# 更新 conftest.py
def pytest_configure(config):
"""配置 pytest"""
config.performance_monitor = PerformanceMonitor()
def pytest_runtest_teardown(item):
"""測試結束後記錄時間"""
duration = time.time() - getattr(item, 'start_time', 0)
monitor = item.config.performance_monitor
monitor.record_duration(item.nodeid, duration)
def pytest_terminal_summary(terminalreporter, config):
"""測試會話結束時輸出報告"""
monitor = config.performance_monitor
print(monitor.generate_report())
# 建立 tests/day24/retry_plugin.py
import pytest
from typing import Dict
class RetryPlugin:
"""重試插件"""
def __init__(self, max_retries: int = 3):
self.max_retries = max_retries
self.retry_count: Dict[str, int] = {}
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(self, item, call):
"""處理測試報告"""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
test_id = item.nodeid
if test_id not in self.retry_count:
self.retry_count[test_id] = 0
if self.retry_count[test_id] < self.max_retries:
self.retry_count[test_id] += 1
print(f"\n🔄 重試測試 (第 {self.retry_count[test_id]} 次)")
report.outcome = "passed"
# 建立 tests/day24/cleanup_plugin.py
from pathlib import Path
from typing import Set
class CleanupManager:
"""清理管理器"""
def __init__(self, test_dir: str = "test_output"):
self.test_dir = Path(test_dir)
self.created_files: Set[Path] = set()
def register_file(self, filepath: Path):
"""註冊需要清理的檔案"""
self.created_files.add(filepath)
def cleanup(self):
"""執行清理"""
for filepath in self.created_files:
if filepath.exists():
filepath.unlink()
print(f"🗑️ 刪除檔案: {filepath}")
# Hook 實作
def pytest_configure(config):
"""初始化清理管理器"""
config.cleanup_manager = CleanupManager()
def pytest_sessionfinish(session):
"""會話結束時執行清理"""
session.config.cleanup_manager.cleanup()
# 建立 tests/day24/marker_plugin.py
import pytest
def pytest_configure(config):
"""註冊自定義標記"""
config.addinivalue_line("markers", "critical: 標記關鍵測試")
config.addinivalue_line("markers", "slow: 標記慢速測試")
def pytest_runtest_setup(item):
"""根據標記設置測試"""
markers = {marker.name for marker in item.iter_markers()}
if "critical" in markers:
print(f"\n⚠️ 執行關鍵測試: {item.name}")
if "slow" in markers:
print(f"\n🐌 執行慢速測試: {item.name}")
# 測試範例
# tests/day24/test_with_markers.py
@pytest.mark.critical
def test_critical_feature():
"""關鍵功能測試"""
assert True
@pytest.mark.slow
def test_slow_operation():
"""慢速操作測試"""
import time
time.sleep(0.5)
assert True
# 建立 tests/day24/report_generator.py
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any
class TestReportGenerator:
"""測試報告生成器"""
def __init__(self, output_dir: str = "test_reports"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
self.test_results: List[Dict[str, Any]] = []
self.session_data: Dict[str, Any] = {}
def start_session(self):
"""開始測試會話"""
self.session_data = {
"start_time": datetime.now().isoformat(),
"test_count": 0,
"passed": 0,
"failed": 0,
"skipped": 0
}
def add_test_result(self, test_data: Dict[str, Any]):
"""添加測試結果"""
self.test_results.append(test_data)
self.session_data["test_count"] += 1
status = test_data["outcome"]
if status in ["passed", "failed", "skipped"]:
self.session_data[status] += 1
def generate_report(self):
"""生成文字報告"""
report = [
"\n" + "="*50,
"📊 測試執行報告",
"="*50,
f"總測試數: {self.session_data.get('test_count', 0)}",
f"✅ 通過: {self.session_data.get('passed', 0)}",
f"❌ 失敗: {self.session_data.get('failed', 0)}",
]
return "\n".join(report)
# 完整的 Hook 整合
# 更新 conftest.py
def pytest_configure(config):
"""配置報告生成器"""
config.report_generator = TestReportGenerator()
def pytest_sessionstart(session):
"""開始測試會話"""
session.config.report_generator.start_session()
def pytest_runtest_makereport(item, call):
"""收集測試結果"""
if call.when == "call":
generator = item.config.report_generator
test_data = {
"name": item.nodeid,
"outcome": "passed" if call.excinfo is None else "failed",
"duration": call.duration,
}
generator.add_test_result(test_data)
def pytest_sessionfinish(session):
"""生成最終報告"""
generator = session.config.report_generator
print(generator.generate_report())
試著建立快取管理 Hook,思考測試要點:清空測試快取、記錄快取使用、警告快取未命中、確保測試隔離。
今天我們學習了如何管理測試生命週期:
✅ 效能監控、失敗重試、資源清理、標記處理
記住:好的生命週期管理讓測試更穩定可靠! 💪