199 lines
4.7 KiB
Markdown
199 lines
4.7 KiB
Markdown
---
|
||
name: python-testing
|
||
description: 使用 pytest 的 Python 測試策略,涵蓋 TDD 方法論、Fixtures、Mocking、參數化 (Parametrization) 以及覆蓋率要求。
|
||
---
|
||
|
||
# Python 測試模式 (Python Testing Patterns)
|
||
|
||
使用 pytest、TDD 方法論與最佳實踐的 Python 應用程式全面測試策略。
|
||
|
||
## 何時啟用
|
||
|
||
- 編寫新的 Python 程式碼(遵循 TDD:紅燈、綠燈、重構)。
|
||
- 為 Python 專案設計測試套件。
|
||
- 審核 Python 測試覆蓋率。
|
||
- 設置測試基礎設施。
|
||
|
||
## 核心測試哲學
|
||
|
||
### 測試驅動開發 (TDD)
|
||
|
||
務必遵循 TDD 循環:
|
||
|
||
1. **紅燈 (RED)**:為預期行為撰寫一個會失敗的測試。
|
||
2. **綠燈 (GREEN)**:編寫最少量的程式碼使測試通過。
|
||
3. **重構 (REFACTOR)**:在保持測試通過的情況下優化程式碼。
|
||
|
||
```python
|
||
# 步驟 1:撰寫失敗的測試 (RED)
|
||
def test_add_numbers():
|
||
result = add(2, 3)
|
||
assert result == 5
|
||
|
||
# 步驟 2:撰寫最小實作 (GREEN)
|
||
def add(a, b):
|
||
return a + b
|
||
|
||
# 步驟 3:如有需要則重構 (REFACTOR)
|
||
```
|
||
|
||
### 覆蓋率要求
|
||
|
||
- **目標**:80% 以上的程式碼覆蓋率。
|
||
- **關鍵路徑**:必須達到 100% 覆蓋率。
|
||
- 使用 `pytest --cov` 來衡量覆蓋率。
|
||
|
||
```bash
|
||
pytest --cov=mypackage --cov-report=term-missing --cov-report=html
|
||
```
|
||
|
||
## pytest 基礎
|
||
|
||
### 斷言 (Assertions)
|
||
|
||
```python
|
||
# 相等性
|
||
assert result == expected
|
||
|
||
# 真值判斷
|
||
assert result # 真值 (Truthy)
|
||
assert result is True # 嚴格等於 True
|
||
assert result is None # 嚴格等於 None
|
||
|
||
# 成員資格
|
||
assert item in collection
|
||
|
||
# 類型檢查
|
||
assert isinstance(result, str)
|
||
|
||
# 例外測試
|
||
with pytest.raises(ValueError):
|
||
raise ValueError("錯誤訊息")
|
||
|
||
# 檢查例外訊息內容
|
||
with pytest.raises(ValueError, match="無效的輸入"):
|
||
raise ValueError("提供的輸入無效")
|
||
```
|
||
|
||
## Fixtures (測試夾具)
|
||
|
||
### 帶有設置與清理的 Fixture
|
||
|
||
```python
|
||
@pytest.fixture
|
||
def database():
|
||
"""帶有設置與清理 (Setup/Teardown) 的 Fixture。"""
|
||
# 設置 (Setup)
|
||
db = Database(":memory:")
|
||
db.create_tables()
|
||
|
||
yield db # 提供給測試案例使用
|
||
|
||
# 清理 (Teardown)
|
||
db.close()
|
||
|
||
def test_database_query(database):
|
||
"""測試資料庫操作。"""
|
||
result = database.query("SELECT * FROM users")
|
||
assert len(result) > 0
|
||
```
|
||
|
||
### 作用域 (Scopes) 與 conftest.py
|
||
|
||
- **function** (預設):每個測試執行一次。
|
||
- **module**:每個模組執行一次。
|
||
- **session**:每次測試會話執行一次。
|
||
- 使用 `tests/conftest.py` 來定義跨檔案共享的 Fixtures。
|
||
|
||
## 參數化 (Parametrization)
|
||
|
||
```python
|
||
@pytest.mark.parametrize("input,expected", [
|
||
("hello", "HELLO"),
|
||
("world", "WORLD"),
|
||
("PyThOn", "PYTHON"),
|
||
])
|
||
def test_uppercase(input, expected):
|
||
"""測試會執行 3 次,每次使用不同的輸入。"""
|
||
assert input.upper() == expected
|
||
```
|
||
|
||
## Mocking 與 Patching
|
||
|
||
```python
|
||
from unittest.mock import patch, Mock
|
||
|
||
@patch("mypackage.external_api_call")
|
||
def test_with_mock(api_call_mock):
|
||
"""測試模擬外部 API。"""
|
||
api_call_mock.return_value = {"status": "success"}
|
||
|
||
result = my_function()
|
||
|
||
api_call_mock.assert_called_once()
|
||
assert result["status"] == "success"
|
||
```
|
||
|
||
## 測試非同步程式碼 (Async Code)
|
||
|
||
需要 `pytest-asyncio` 外掛:
|
||
|
||
```python
|
||
import pytest
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_async_function():
|
||
"""測試非同步函式。"""
|
||
result = await async_add(2, 3)
|
||
assert result == 5
|
||
```
|
||
|
||
## 測試組織與最佳實踐
|
||
|
||
### 目錄結構建議
|
||
|
||
```
|
||
tests/
|
||
├── conftest.py # 共享 Fixtures
|
||
├── unit/ # 單元測試
|
||
│ └── test_models.py
|
||
├── integration/ # 整合測試
|
||
│ └── test_api.py
|
||
└── e2e/ # 端到端測試
|
||
└── test_user_flow.py
|
||
```
|
||
|
||
### 應做事項 (DO)
|
||
- **遵循 TDD**:先寫測試再寫程式碼。
|
||
- **單一功能測試**:每個測試案例僅驗證一個行為。
|
||
- **命名具備描述性**:例如 `test_user_login_with_invalid_credentials_fails`。
|
||
- **模擬外部依賴**:不要依賴外部服務或網路。
|
||
|
||
### 避免事項 (DON'T)
|
||
- **不要測試實作細節**:應測試行為而非內部邏輯。
|
||
- **不要在測試中使用複雜的判斷式**:保持測試簡單明瞭。
|
||
- **不要忽視失敗的測試**:所有測試必須全部通過。
|
||
- **不要在測試之間共享狀態**:測試案例應保持獨立。
|
||
|
||
## 常用指令
|
||
|
||
```bash
|
||
# 執行所有測試
|
||
pytest
|
||
|
||
# 執行特定檔案
|
||
pytest tests/test_utils.py
|
||
|
||
# 執行特定測試案例
|
||
pytest tests/test_utils.py::test_function
|
||
|
||
# 帶有詳細輸出
|
||
pytest -v
|
||
|
||
# 執行直到第一次失敗即停止
|
||
pytest -x
|
||
|
||
# 重新執行上次失敗的測試
|
||
pytest --lf
|
||
```
|