---
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
```
