跳到主要內容

測試驅動開發

在編寫實現代碼之前,在實現任何功能或修復錯誤時使用。通過測試優先的方法強制執行 RED-GREEN-REFACTOR(紅-綠-重構)循環。

技能元數據

來源捆綁(默認安裝)
路徑skills/software-development/test-driven-development
版本1.1.0
作者Hermes Agent(改編自 obra/superpowers)
許可證MIT
標籤testing, tdd, development, quality, red-green-refactor
相關技能systematic-debugging, writing-plans, subagent-driven-development

參考:完整 SKILL.md

信息

以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理看到的指令。

測試驅動開發 (TDD)

概述

先寫測試。觀察其失敗。編寫最小化的代碼以通過測試。

核心原則: 如果你沒有觀察到測試失敗,你就不知道它是否測試了正確的內容。

違反規則的字面意思就是違反規則的精神實質。

何時使用

始終使用:

  • 新功能
  • 錯誤修復
  • 重構
  • 行為變更

例外情況(請先詢問用戶):

  • 一次性原型
  • 生成的代碼
  • 配置文件

想著“就這一次跳過 TDD”?停下。那是自我合理化。

鐵律

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

在測試之前編寫代碼?刪除它。重新開始。

絕無例外:

  • 不要將其保留為“參考”
  • 不要在編寫測試時“調整”它
  • 不要查看它
  • 刪除意味著徹底刪除

根據測試從頭實現。僅此而已。

Red-Green-Refactor 循環

RED — 編寫失敗的測試

編寫一個展示預期行為的最小化測試。

好的測試:

def test_retries_failed_operations_3_times():
attempts = 0
def operation():
nonlocal attempts
attempts += 1
if attempts < 3:
raise Exception('fail')
return 'success'

result = retry_operation(operation)

assert result == 'success'
assert attempts == 3

名稱清晰,測試真實行為,只測一件事。

壞的測試:

def test_retry_works():
mock = MagicMock()
mock.side_effect = [Exception(), Exception(), 'success']
result = retry_operation(mock)
assert result == 'success' # What about retry count? Timing?

名稱模糊,測試的是 mock 而非真實代碼。

要求:

  • 每個測試只針對一種行為
  • 清晰描述性的名稱(名稱中有“and”?將其拆分)
  • 真實代碼,而非 mocks(除非確實不可避免)
  • 名稱描述行為,而非實現細節

驗證 RED — 觀察其失敗

強制要求。絕不可跳過。

# Use terminal tool to run the specific test
pytest tests/test_feature.py::test_specific_behavior -v

確認:

  • 測試失敗(而非因拼寫錯誤導致的報錯)
  • 失敗消息符合預期
  • 失敗是因為功能缺失

測試立即通過? 你正在測試現有行為。修正測試。

測試報錯? 修復錯誤,重新運行直到它正確失敗。

GREEN — 最小化代碼

編寫通過測試的最簡單代碼。僅此而已,不多不少。

好的做法:

def add(a, b):
return a + b # Nothing extra

壞的做法:

def add(a, b):
result = a + b
logging.info(f"Adding {a} + {b} = {result}") # Extra!
return result

不要添加功能、重構其他代碼或進行超出測試範圍的“改進”。

在 GREEN 階段作弊是可以接受的:

  • 硬編碼返回值
  • 複製粘貼
  • 重複代碼
  • 忽略邊緣情況

我們將在 REFACTOR 階段修復這些問題。

驗證 GREEN — 觀察其通過

強制要求。

# Run the specific test
pytest tests/test_feature.py::test_specific_behavior -v

# Then run ALL tests to check for regressions
pytest tests/ -q

確認:

  • 測試通過
  • 其他測試仍然通過
  • 輸出乾淨(無錯誤、無警告)

測試失敗? 修復代碼,而不是修復測試。

其他測試失敗? 立即修復迴歸問題。

REFACTOR — 清理

僅在變綠(Green)之後:

  • 消除重複
  • 改進命名
  • 提取輔助函數
  • 簡化表達式

在整個過程中保持測試通過。不要添加新行為。

如果在重構期間測試失敗: 立即撤銷。採取更小的步驟。

重複

針對下一個行為編寫下一個失敗的測試。一次一個循環。

為什麼順序很重要

“我會在之後編寫測試來驗證它是否有效”

在代碼之後編寫的測試會立即通過。立即通過證明不了什麼:

  • 可能測試了錯誤的內容
  • 可能測試的是實現細節,而非行為
  • 可能遺漏了你忘記的邊緣情況
  • 你從未看到它捕獲錯誤

測試優先迫使您看到測試失敗,從而證明它確實在測試某些內容。

“我已經手動測試了所有邊緣情況”

手動測試是隨意的。你認為你測試了一切,但:

  • 沒有記錄你測試了什麼
  • 代碼更改時無法重新運行
  • 在壓力下容易遺漏情況
  • “我試的時候它是有效的” ≠ 全面覆蓋

自動化測試是系統化的。它們每次都以相同的方式運行。

“刪除 X 小時的工作成果是浪費”

這是沉沒成本謬誤。時間已經流逝。你現在的選擇是:

  • 刪除並使用 TDD 重寫(高置信度)
  • 保留它並在之後添加測試(低置信度,可能存在錯誤)

真正的“浪費”是保留你無法信任的代碼。

“TDD 是教條主義的,務實意味著適應”

TDD 本身就是務實的:

  • 在提交前發現錯誤(比事後調試更快)
  • 防止迴歸(測試能立即捕獲破壞)
  • 記錄行為(測試展示如何使用代碼)
  • 支持重構(自由更改,測試能捕獲破壞)

所謂的“務實”捷徑 = 在生產環境中調試 = 更慢。

“事後測試也能達到相同的目標——重要的是精神而非形式”

否。後寫測試回答的是“這段代碼做了什麼?” 先寫測試回答的是“這段代碼應該做什麼?”

後寫測試會受到你具體實現的偏見影響。你測試的是你構建的東西,而不是需求所要求的東西。先寫測試迫使你在實現之前發現邊界情況。

常見的合理化藉口

藉口現實
“太簡單了,不用測試”簡單的代碼也會出錯。測試只需 30 秒。
“我稍後再測試”測試立即通過證明不了任何東西。
“後寫測試能達到相同的目標”後寫測試 = “這段代碼做了什麼?” 先寫測試 = “這段代碼應該做什麼?”
“已經手動測試過了”臨時測試 ≠ 系統測試。沒有記錄,無法重新運行。
“刪除 X 小時的工作成果是浪費”沉沒成本謬誤。保留未經驗證的代碼就是技術債務。
“保留作為參考,先寫測試”你會去適配它。那就是後寫測試。刪除意味著徹底刪除。
“需要先探索一下”沒問題。丟棄探索性代碼,從 TDD 開始。
“測試難寫 = 設計不清晰”傾聽測試的聲音。難以測試 = 難以使用。
“TDD 會拖慢我的速度”TDD 比調試更快。務實的做法是先寫測試。
“手動測試更快”手動測試無法證明邊界情況。每次更改你都得重新測試。
“現有代碼沒有測試”你正在改進它。為你觸動的代碼添加測試。

危險信號 — 停止並重新開始

如果你發現自己有以下任何行為,請刪除代碼並使用 TDD 重新開始:

  • 先寫代碼後寫測試
  • 在實現之後編寫測試
  • 測試在首次運行時立即通過
  • 無法解釋測試失敗的原因
  • “稍後”才添加測試
  • 合理化“就這一次”
  • “我已經手動測試過了”
  • “後寫測試能達到相同的目的”
  • “保留作為參考”或“適配現有代碼”
  • “已經花了 X 小時,刪除太浪費了”
  • “TDD 太教條,我是在務實行事”
  • “這種情況不同,因為……”

所有這些均意味著:刪除代碼。使用 TDD 重新開始。

驗證清單

在標記工作完成之前:

  • 每個新函數/方法都有測試
  • 在實現之前觀察到每個測試失敗
  • 每個測試都因預期原因失敗(缺少功能,而非拼寫錯誤)
  • 編寫了通過每個測試所需的最小化代碼
  • 所有測試均通過
  • 輸出乾淨(無錯誤、無警告)
  • 測試使用真實代碼(僅在不可避免時使用 mock)
  • 覆蓋了邊界情況和錯誤

無法勾選所有選項?你跳過了 TDD。重新開始。

遇到困境時

問題解決方案
不知道如何測試寫出你期望的 API。先編寫斷言。詢問用戶。
測試過於複雜設計過於複雜。簡化接口。
必須 mock 所有內容代碼耦合度過高。使用依賴注入。
測試設置龐大提取輔助函數。仍然複雜?簡化設計。

Hermes Agent 集成

運行測試

在每一步使用 terminal 工具運行測試:

# RED — verify failure
terminal("pytest tests/test_feature.py::test_name -v")

# GREEN — verify pass
terminal("pytest tests/test_feature.py::test_name -v")

# Full suite — verify no regressions
terminal("pytest tests/ -q")

配合 delegate_task

在分派子代理進行實現時,在目標中強制要求 TDD:

delegate_task(
goal="Implement [feature] using strict TDD",
context="""
Follow test-driven-development skill:
1. Write failing test FIRST
2. Run test to verify it fails
3. Write minimal code to pass
4. Run test to verify it passes
5. Refactor if needed
6. Commit

Project test command: pytest tests/ -q
Project structure: [describe relevant files]
""",
toolsets=['terminal', 'file']
)

配合 systematic-debugging

發現 Bug?編寫一個復現該 Bug 的失敗測試。遵循 TDD 循環。該測試證明了修復的有效性並防止迴歸。

切勿在沒有測試的情況下修復 Bug。

測試反模式

  • 測試 mock 行為而非真實行為 — mock 應用於驗證交互,而非替代被測系統
  • 測試實現細節 — 測試行為/結果,而非內部方法調用
  • 僅測試正常路徑 — 始終測試邊界情況、錯誤和邊界條件
  • 脆弱的測試 — 測試應驗證行為,而非結構;重構不應導致測試失敗

最終規則

Production code → test exists and failed first
Otherwise → not TDD

未經用戶明確許可,不得有任何例外。