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