iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
AI Engineering

同一把尺,30 天橫評 AI Agent Skill:從單篇實測到一份能查的選用指南系列 第 15 篇

Day 15 | test-driven-development:規定「沒有失敗的測試就不准寫程式」的技能

  • 分享至 

  • xImage
  •  

昨天介紹了 superpowers 這套技能包裡的 systematic-debugging,也測到一個不算好聽的結果:規則寫得再明確,這次實測下來還是被跳過了。今天想接著介紹同一包裡的另一個技能 test-driven-development,用意不是要挽回昨天的成績,是這個技能剛好給了一個公平的機會,換一種任務類型(不是修 bug,是做一個全新功能)重新看一次同一個問題:規則說了算,還是實際上真的算。這個技能本身也值得單獨認識——它處理的是「測試先行」這個大家都懂道理、卻常常做不到的習慣,跟昨天的除錯流程剛好是開發生命週期裡前後相接的兩段。

test-driven-development:一條鐵律,三步循環

test-driven-development 是什麼、要解決什麼問題

多數工程師都聽過「先寫測試再寫程式」這個說法,但實務上常常變成「先寫程式,測試補在後面」,因為先寫測試感覺比較慢、比較繞。test-driven-development 這個技能要處理的正是這個落差:把「測試先行」寫成一條不能商量的鐵律,而不是留給每次臨場判斷。

核心規則,原文照抄:「NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST」——沒有一個先失敗的測試,就不准寫功能程式碼。如果不小心先寫了程式碼才想到要補測試,規則講得很絕:「Write code before the test? Delete it. Start over.」不是留著當參考、不是拿舊程式碼去套測試,是整段刪掉重來。

流程本身是經典的紅燈-綠燈-重構循環:

  • 紅燈:寫一個測試,跑一次,親眼看著它失敗——技能文件特別強調「如果沒看著測試真的失敗過,就不知道這個測試到底測了對不對的東西」。
  • 綠燈:寫剛好能讓這個測試通過的最少程式碼,不多寫。
  • 重構:確定測試還是綠燈的前提下,整理程式碼。

適用範圍寫得很寬——新功能、修 bug、重構、改行為,「一律都要」。例外只有三種:一次性的原型、產生出來的程式碼、設定檔,而且要先問過使用者才能跳過。文件裡也預先擋了一句最常見的偷懶念頭:「這次跳過 TDD 就好」→ 這就是在合理化,本身就是要停下來的訊號。這個設計思路其實跟昨天介紹的 systematic-debugging 一脈相承——同一套技能包裡,好幾個技能都在做同一件事:把「聽起來很合理的偷懶藉口」預先列出來、逐條反駁,試圖用文字把偷工減料的空間先堵起來。

跟 systematic-debugging 不一樣的地方是使用時機更明確:後者要判斷「這個問題是不是真的需要根因調查」還有一點模糊空間,前者的判準幾乎沒有模糊地帶——只要是要寫新的功能程式碼,規則就適用,「沒有測試」本身不是一個可以合理化的例外。

紅燈、綠燈、重構:一個循環

找一個新功能來試,不是修 bug

昨天測 systematic-debugging 用的是「修一個藏在細節裡的 bug」,今天換成「從零開始做一個功能」,一來避開重複的任務類型,二來剛好對上這個技能自己講的使用時機——「Use when implementing any feature or bugfix, before writing implementation code」,新功能正是它的主場。

任務給得很直接,不提示怎麼開發:「幫我用 Python 實作一個函式 calculate_shipping_discount(cart_total),規則:滿 2000(含)打 8 折、滿 1000(含)未滿 2000 打 9 折,其他不打折,回傳折扣後金額。這是全新功能,還沒有任何程式碼,自己決定檔案怎麼組織、怎麼開發。」一組在全新的空資料夾裡跑,沒裝這個技能包;另一組先啟用 superpowers,同樣在一個全新的空資料夾裡跑。兩個資料夾事前都是空的,這樣工具呼叫的順序可以直接從紀錄裡讀出來,不用猜測試檔跟功能檔誰先誰後。

沒裝技能包時:測試先行,但是整批寫

沒裝 superpowers 那組,交出的開發順序是:先寫好一個包含 7 個測試案例的 test_shipping.py,跑一次,因為 shipping.py 還不存在而全部失敗;接著寫出完整的 shipping.py,再跑一次,7 個全過。完成後還另外做了一次邊界檢查——把兩個 >= 暫時改成 >,重跑確認「剛好 1000」「剛好 2000」這兩個邊界測試真的會失敗,證明測試抓得到門檻寫錯,確認完再改回來。

這個順序我自己對照過實際的工具呼叫紀錄,測試檔確實先寫、先跑、先失敗,再寫功能檔——這組雖然沒有這個技能,自己就採用了「測試先行」的寫法,只是顆粒度是整批的:七個案例一次寫完、一次驗證失敗、再一次性把功能寫完。最終的實作是這樣:

def calculate_shipping_discount(cart_total: float) -> float:
    """回傳折扣後金額:滿 2000(含)打 8 折,滿 1000(含)打 9 折,其餘不打折。"""
    if cart_total >= 2000:
        return cart_total * 0.8
    if cart_total >= 1000:
        return cart_total * 0.9
    return cart_total

回報裡還老實提了兩件規格沒寫清楚的事:回傳值沒有四捨五入(1999.99 會變成 1799.991,可能帶浮點誤差)、負數金額目前會落到「不打折」原樣回傳,兩者都留給後續決定要不要改。

裝了技能包時:規則真的被讀了,紅綠循環逐條走完

裝了 superpowers 那組,這次工具呼叫紀錄裡真的出現了 Skill 的呼叫:{"skill": "superpowers:test-driven-development"}——不是像昨天那樣只在系統提示裡被提及名字,是真的被叫用,技能檔案裡「NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST」跟「Red-Green-Refactor」這些原文,也確實出現在對話紀錄裡,代表內容真的被讀進去了。

開發過程也對得上規則描述的樣子,而且顆粒度比沒裝技能包那組細很多:

  1. 先建一個空的 shipping.py,寫第一個測試(金額 500 應該不打折),跑一次,因為函式還沒實作而失敗;寫最小的程式碼 return cart_total 讓它通過。
  2. 加第二個測試(2000 應該變 1600),跑一次先失敗;補上 8 折那段邏輯,再跑通過。
  3. 加第三個測試(1000 應該變 900),跑一次先失敗;補上 9 折那段邏輯,再跑通過。
  4. 剩下幾個邊界案例(0、999.99、1500、1999.99、2500)用現有程式碼一次就會通過,這組另外想到一個問題:「這些測試能不能真的抓到錯」看不出來,所以動手把程式碼故意改壞 6 種版本(門檻寫錯、>= 改 >、折數對調),逐一確認每一種都至少讓一個測試變紅,最後把程式碼還原。

最前面兩步的實際指令長這樣(節錄自對話紀錄裡的 Bash 呼叫):

# 第一步:只放一個測試,讓它先紅
touch shipping.py && cat > test_shipping.py <<'EOF'
def test_below_1000_has_no_discount() -> None:
    assert calculate_shipping_discount(500) == pytest.approx(500)
EOF
pytest -q   # → ImportError,函式還不存在

# 寫最少的程式碼讓它綠
cat > shipping.py <<'EOF'
def calculate_shipping_discount(cart_total: float) -> float:
    return cart_total
EOF
pytest -q   # → 1 passed

接著每加一個新案例(2000 打 8 折、1000 打 9 折),都重複「先跑一次看紅燈、再補程式碼看綠燈」這個動作,不是一口氣把邏輯全寫完再驗證。這個過程我自己重新做了一次獨立驗證:兩組的 shipping.py 最終邏輯完全一樣,各自的測試也都通過;那 6 種故意改壞的版本,我重新跑過一次,每一種確實都至少讓一條測試失敗,跟這組自己回報的結果一致。這組還老實交代了一個過程中的小插曲,原文照抄:「突變測試的第一輪數字不可信。程式已經還原成正確版本,測試卻還是報 2 failed。我推測是 Python 的 bytecode 快取沒有更新……但刪掉 __pycache__ 之後,第一次重跑還是出現過一次 2 failed,所以真正原因沒有確認。」它沒有把這個異常悄悄蓋過去,也沒有硬掰一個聽起來合理的解釋交差,而是把「懷疑的原因」「試過的排除方法」「仍然沒有查證清楚的部分」都攤開來講,關掉快取重跑確認 6 種改壞版本都能被抓到之後,才敢下結論。這種「數字一開始怪怪的、先查清楚原因再下結論,查不清楚就照實講」的態度,跟規則裡「先驗證再宣稱完成」的精神是同一件事,只是這次不是規則逐字要求的內容,是被技能啟動後連帶展現出來的做事方式。

測試先行的兩種顆粒度

我們的觀點:跟昨天放在一起看,才看得出差別在哪

邏輯一樣正確,過程的嚴謹度不同

把這兩天的結果放在一起,會發現同一套強制調度規則,命中率並不是固定的:昨天測「修 bug」任務,規則說了「一定要先用 systematic-debugging」,實測下來完全沒被叫到;今天測「做新功能」任務,規則說了「一定要先用 test-driven-development」,這次真的被叫到,內容也真的被讀進去,而且落實得比沒裝技能包那組更細緻。

這代表一件事:「這套強制規則靠不靠得住」沒有一個固定答案,會因為任務種類、甚至同一種任務的不同次執行而不同。 兩天都只跑了一次,樣本數都是 1,沒辦法算出一個可靠的機率,但這已經足夠推翻「這套技能包的強制力要嘛全都有效、要嘛全都沒用」這種過度簡化的想法。比較合理的態度是:裝了不等於保證,但也不等於沒用,要看那一次有沒有真的被叫到,而不是套用某一天測到的結果去代表全部。

為什麼昨天沒被叫到、今天被叫到,今天的資料沒辦法給出確定答案,但有一個值得記下來的猜測方向:昨天的任務是「使用者回報一個奇怪的問題,幫我修好」,這種敘述本身沒有直接點名「bug」這個字;今天的任務是「幫我實作一個函式……這是全新功能」,「實作」「全新功能」這幾個詞,可能比昨天的委婉敘述更直接對上 using-superpowers 規則裡「implementing any feature」這條描述。如果這個猜測成立,代表任務描述本身的用字,可能會影響規則判斷「這個技能適不適用」的準確度——這跟規則內容寫得多嚴謹是兩回事,是任務描述跟規則裡的觸發詞有沒有對上的問題。這只是一個假設,需要用更多不同措辭的任務去測才能確認,不是今天就能下的結論。

值得一提的是,今天裝了技能包那組跑的紅綠循環,比沒裝技能包那組更接近規則原本教的樣子——一次只處理一個測試案例,看著它紅、再讓它綠,而不是一次寫完一整批測試再一次寫完整個實作。兩種做法最後寫出來的程式碼一模一樣,但過程的嚴謹程度確實不同,這正是這個技能真正想達到的效果:不只是「有沒有測試」,是「測試跟程式碼之間,有沒有真的一步一步互相驗證過」。

這也點出「規則被照做」跟「結果變好」不必然是同一件事——今天這個功能夠簡單,兩種顆粒度最後寫出來的邏輯完全一樣,看不出紅綠循環比整批寫測試多防到了什麼真正的錯。細顆粒度真正的價值,比較可能出現在需求會邊做邊修正、或是一次寫太多程式碼容易埋錯的情況,這點今天的題目沒辦法直接證明,只能說這次觀察到的差異是「過程更嚴謹」,不是「結果更正確」。

同一套強制規則,兩天測出兩種結果

這對你有什麼用

  • 同一套強制調度規則,不同任務、不同次執行,命中率不一樣。 別把「這個技能包昨天沒被叫到」直接套用成「這個技能包不可靠,不用裝了」,也別把「今天被叫到了」直接當成「以後每次都會這樣」,兩種結論都太快。
  • 想知道規則有沒有真的被照做,看工具呼叫紀錄裡有沒有出現 Skill 這個呼叫,比看最終產出可靠。 今天這組能看到明確的 {"skill": "superpowers:test-driven-development"} 記錄,這是比對話文字裡「有沒有提到技能名字」更硬的證據。
  • 就算沒裝這個技能包,測試先行這個做法本身也不是遙不可及。 今天沒裝的那組自己就採用了「先寫測試、跑到失敗、再寫實作」的順序,只是顆粒度比較粗。如果你想要的只是「大致上測試先行」,不一定要靠這個技能包才做得到;如果你想要的是「每加一個案例就驗證一次紅綠」這種更嚴謹的節奏,這個技能提供的具體流程還是有實質差異。
  • 技能真的被叫到時,額外做的事往往超出「規則要求的最低限度」。 今天這組不只做了紅綠循環,還自己想到要做突變測試(故意把程式碼改壞確認測試抓得到),這不是規則裡寫死要求的,是被技能內容啟動後,連帶展現出的品質意識。
  • 遇到跑出來的數字怪怪的,比較可靠的做法是講出「懷疑的原因」跟「還沒查證的部分」,不是硬掰一個解釋蓋過去。 今天那組面對突變測試第一輪數字異常時的處理方式值得參考:先老實承認「這個結果不可信」,再去查可能的原因,查不到底就照實講查不到,不假裝已經搞懂。

誠實交代這次測試的限制

  • 這次的「基線」不是完全乾淨的對照組。這個專案裝了記憶系統,每次開新對話都會透過 SessionStart 掛鉤帶入最近的活動摘要,我事後查對話紀錄才發現,沒裝 superpowers 那組的系統提示裡,其實混進了一句「今天在測 test-driven-development」——這句話跟我平常自己開新對話時看到的記憶摘要是同一種機制,不是刻意安排的,但確實代表這組不是完全不知道今天主題是什麼的乾淨基線。雖然沒有技能被實際呼叫、也沒有規則內容被讀入,這點在解讀「基線自己就想到測試先行」這個結果時要打一點折扣——不能排除這句話多少提示了一點方向。之後如果要做更嚴謹的對照,應該在一個沒有裝這套記憶系統的環境跑基線,才是真正乾淨的版本。
  • 樣本數是兩天各 1 次,「昨天沒被叫到、今天被叫到」這個對比很鮮明,但不足以算出任何機率,也沒辦法排除純粹是這兩次剛好落在不同側的運氣。
  • 這次一樣是用 headless(claude -p)模式測的,互動模式下技能呼叫的行為有沒有不同,仍然沒有對照測過。
  • 這個功能本身不難,兩組寫出來的最終邏輯一模一樣,沒有測到「紅綠循環比整批寫測試更嚴謹」這件事,在更複雜、更容易寫錯的功能上,會不會真的減少 bug,今天的題目沒辦法回答。
  • 沒裝技能包那組自己做的突變測試(改 >= 為 >)只驗證了兩個門檻,裝了技能包那組做了 6 種改壞版本,涵蓋範圍更完整,這個差異有可能只是這次剛好想到的多寡不同,不是「有沒有技能」造成的系統性差異,需要更多樣本才能確定。

上一篇
Day 14 | superpowers:把「除錯先查根因」寫成規則的技能包
下一篇
Day 16 | skill-scanner:一個把自己「抓不到多少」講清楚的 Skill 安全掃描工具
系列文
同一把尺,30 天橫評 AI Agent Skill:從單篇實測到一份能查的選用指南 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言