測試原則
語言特定參考
如需特定語言的測試模式,請一併閱讀:
- TypeScript (Vitest/RTL/Playwright): references/typescript.md
核心測試理念
- 測試是一級程式碼:保持測試品質與生產程式碼同等
- 快速回饋:測試應快速執行並提供即時回饋
- 可靠性:測試應具有確定性且可重現
- 獨立性:每個測試應獨立執行
TDD 流程 [所有程式碼變更強制執行]
對每次程式碼變更執行此流程:
RED 階段
步驟 1:撰寫定義預期行為的測試 步驟 2:執行測試 步驟 3:確認測試失敗(若通過,則測試有誤)
GREEN 階段
步驟 1:撰寫讓測試通過的最小程式碼 步驟 2:執行測試 步驟 3:確認測試通過
REFACTOR 階段
步驟 1:改善程式碼品質(消除重複、改善命名) 步驟 2:執行測試 步驟 3:確認測試仍通過
VERIFY 階段 [強制執行 - 需零錯誤]
步驟 1:執行語言/專案的所有品質檢查指令 步驟 2:修正所有錯誤,直到所有指令都以零錯誤通過 步驟 3:確認無回歸問題
強制執行:在任一品質檢查失敗時,不得進入下一階段
TDD 例外情況(不需 TDD)
- 純設定檔
- 僅文件變更
- 緊急修復(但完成後需立即補上測試)
- 探索性測試(合併前需棄用或重寫並補上測試)
- 建置/部署腳本(除非包含商業邏輯)
品質要求 [強制執行]
涵蓋率
- 將涵蓋率視為尋找未測試區域的診斷訊號,而非目標。目標容易被遊戲化,導致產生無意義的測試。
- 將測試集中在關鍵路徑、商業邏輯以及回歸時會造成影響的行為上。
- 優先處理有意義的斷言,而非涵蓋率數字。任何要求的門檻值來自專案的 CI、任務檔案、工作計畫或設計文件。
測試特徵
所有測試必須:
- 獨立:測試之間無相依性
- 可重現:相同輸入始終產生相同輸出
- 快速:在專案可接受的回饋視窗內完成整個測試套件,並標記會顯著拖慢本地迭代或 CI 的套件
- 自我檢查:無需人工驗證即可清楚顯示通過/失敗
- 即時:在與受測程式碼相近的時間點撰寫
強制執行:不符合任一特徵的測試必須立即修正
測試類型
單元測試
目的:隔離測試個別元件
特徵:
- 測試單一函式、方法或類別
- 快速執行(毫秒級)
- 無外部相依性
- 模擬外部服務
- 佔測試套件的大部分
整合測試
目的:測試元件之間的互動
特徵:
- 同時測試多個元件
- 可能包含資料庫、檔案系統或 API
- 比單元測試慢
- 驗證模組之間的合約
- 佔測試套件的較小部分
端對端 (E2E) 測試
目的:從使用者角度測試完整工作流程
特徵:
- 測試完整應用程式堆疊
- 模擬真實使用者互動
- 最慢的測試類型
- 數量最少
- 信心水準最高
測試金字塔
遵循測試金字塔結構:
/\ <- 少量 E2E 測試(高信心,慢速)
/ \
/ \ <- 部分整合測試(中等信心,中等速度)
/ \
/________\ <- 大量單元測試(快速,基礎)
測試設計原則
AAA 模式(Arrange-Act-Assert)
將每個測試結構化為三個清晰的階段:
// Arrange:設定測試資料與條件
user = createTestUser()
validator = createValidator()
// Act:執行受測程式碼
result = validator.validate(user)
// Assert:驗證預期結果
assert(result.isValid == true)
每個概念一個斷言
- 每個測試案例測試一個行為
- 若測試單一概念,可有多個斷言
- 將不相關的斷言拆分到不同測試中 — 一個測試必須驗證一個行為
描述性測試名稱
測試名稱應清楚描述:
- 測試什麼
- 在什麼條件下
- 預期結果是什麼
建議格式: "當 [條件] 時,應 [預期行為]"
測試獨立性
隔離需求
- 每個測試建立自己的測試資料
- 不依賴執行順序
- 清理自己的狀態
- 隔離執行時通過
設定與清理
- 使用設定鉤子準備測試環境
- 使用清理鉤子釋放資源
- 將設定範圍限定在受測行為所需的資料、相依性與測試固件
- 確保即使測試失敗,清理仍會執行
模擬與測試替身
何時使用模擬
- 模擬外部相依性:API、資料庫、檔案系統
- 模擬慢速操作:網路呼叫、大量運算
- 模擬不可預測行為:隨機值、目前時間
- 模擬不可用服務:第三方服務
模擬原則 [強制執行]
- 在邊界進行模擬,而非內部 — 內部工具使用真實實作
- 將每個模擬限制在測試需要控制或觀察的行為上
- 在相關時驗證模擬期望值
- 對不受您控制的外部函式庫/框架使用轉接器
測試替身類型
- Stub:回傳預先決定的值
- Mock:驗證是否被正確呼叫
- Spy:記錄呼叫的相關資訊
- Fake:簡化的可工作實作
- Dummy:傳入但從不使用
資料層測試
資料存取的模擬限制
模擬可驗證呼叫模式,但無法驗證綱要正確性、查詢正確性或儲存限制。 模擬可能遺漏的問題範例:
- schema drift(綱要漂移)
- 欄位或欄位不匹配
- 錯誤的聯結、篩選或聚合
- 遷移不相容
何時加入真實資料層驗證
在測試以下項目時,使用真實或類生產的資料存取驗證:
- repository 或 DAO 實作
- ORM 對應
- 查詢建構器或原始 SQL
- 依賴於限制或綱要形狀的持久化行為
環境選項
為專案環境選擇最實用的選項:
- 容器化資料庫
- 專用測試資料庫
- 記憶體資料庫(需記錄限制)
- 基於轉接器的本地測試工具
設計對齊
當設計文件包含 Test Boundaries 時,以其為基準決定哪些相依性保持真實,哪些邊界需要隔離。
測試品質實務 [強制執行]
保持測試活躍
- 修正或刪除失敗的測試:立即解決失敗
- 移除註解掉的測試:修正或完全刪除
- 讓測試持續執行:損壞的測試會快速失去價值
- 維護測試套件:視需要重構測試
測試程式碼品質
- 套用與生產程式碼相同的標準
- 使用描述性變數名稱
- 擷取測試輔助函式以減少重複
- 保持測試可讀且可維護
測試輔助函式與工具
- 建立可重複使用的測試資料建構器
- 將通用設定擷取到輔助函式中
- 為複雜場景建立測試工具
- 在測試檔案間適當共享輔助函式
測試什麼
專注於行為
測試可觀察的行為,而非實作細節:
- 良好:測試函式是否回傳預期輸出
- 良好:測試是否呼叫正確的 API 端點
- 不良:測試內部變數是否被設定
- 不良:測試私有方法的呼叫順序
測試邊界情況
始終測試:
- 邊界條件:最小值/最大值、空集合
- 錯誤情況:無效輸入、空值、遺失資料
- 邊緣案例:特殊字元、極端值
- 快樂路徑:正常、預期的使用方式
測試品質標準 [強制執行]
- 字面期望:在斷言中使用硬編碼的字面值 — 期望值 ≠ 模擬回傳值(實作會處理資料)
- 結果驗證:斷言回傳值和狀態,而非呼叫順序
- 有意義的斷言:每個測試必須至少有一個斷言 — 沒有斷言的測試毫無價值
- 僅模擬外部 I/O:模擬資料庫/API/檔案系統,使用真實內部工具
- 邊界涵蓋:包含空值/零值/最大值/錯誤案例以及快樂路徑
強制執行:違反任一標準的測試必須重寫
驗證要求 [VERIFY 階段強制執行]
提交前檢查清單
☐ 所有測試通過 ☐ 無測試被跳過或註解 ☐ 測試中無遺留除錯程式碼 ☐ 當專案、任務檔案、工作計畫或設計文件有定義時,涵蓋率門檻通過 ☐ 測試在合理時間內執行
零容忍政策
- 零失敗測試:立即修正
- 零跳過測試:刪除或修正
- 零不穩定測試:使其確定性
- 零緩慢測試:最佳化或拆分
強制執行:若任一品質檢查失敗,不得繼續任務完成
測試組織
檔案結構
- 鏡像生產結構:測試遵循程式碼組織
- 清楚命名慣例:遵循專案的測試檔案模式
- 邏輯分組:將相關測試分組在一起
- 分離測試類型:單元測試、整合測試、端對端測試放在不同目錄
測試套件組織
tests/
├── unit/ # 快速、隔離的單元測試
├── integration/ # 整合測試
├── e2e/ # 端對端測試
├── fixtures/ # 測試資料與固件
└── helpers/ # 共享測試工具
效能考量
測試速度
- 單元測試:每個 < 100ms
- 整合測試:每個 < 1s
- 完整套件:應可頻繁執行(< 10 分鐘)
常見反模式
立即偵測並消除以下模式:
- 什麼都不測試的測試(始終通過)
- 依賴執行順序的測試
- 依賴外部狀態的測試
- 含有複雜邏輯的測試(需要自己測試的測試)
- 測試實作細節而非可觀察行為
- 過度模擬(僅模擬邊界,使用真實內部實作)
- 測試程式碼重複
不穩定的測試
消除間歇性失敗的測試:
- 移除時間相依性
- 使用確定性資料而非隨機值
- 確保適當清理
- 修正競爭條件 (race condition)
- 讓所有測試具有確定性
回歸測試
- 每次錯誤修正都增加測試
- 維護完整的測試套件
- 定期執行完整套件
- 僅在以下情況刪除測試:被涵蓋的行為不再存在,或相同行為已由更強且位於正確層級的測試所涵蓋
舊有程式碼
- 在重構前加入特徵測試
- 先測試現有行為
- 逐步改善涵蓋率
- 自信地重構