前言¶
讓編程 Agent 寫測試,最常見的情況不是它不肯寫,而是它寫得太快、一次寫太多。你說「加一個結賬功能,順便補測試」,它往往會先把整套測試骨架鋪出來,再一次性把實現填進去。表面上看紅綠都走了,實際上這些測試驗證的是它想象中的接口形狀,而不是用戶真正能觀察到的行爲。內部一重構,測試就碎;斷言跟實現用同一套計算,測試永遠不會失敗。
Matt Pocock 把這件事收成了一個 Agent Skill,名字就叫 tdd。它不負責替你排期、拆票、提交,只規定紅綠循環裏每一圈該怎麼走:測什麼、測在哪、一次只寫一個測試、一次只寫剛好能讓這個測試通過的實現。倉庫 mattpocock/skills 把這類實踐叫「給真正工程師用的 Skill」,tdd 是其中被標成 model-invoked 的工程技能之一:你可以輸入 /tdd,Agent 在「先寫測試」「red-green-refactor」「集成測試」這類任務上也會自己去讀它。
本文根據 GitHub 上的 SKILL.md、配套的 tests.md / mocking.md,以及作者在 aihero.dev 上的技能說明交叉整理,介紹這個 Skill 解決什麼問題、怎麼安裝、怎麼用。
這是什麼¶
tdd 是一份給 Agent 看的 TDD 規程,不是一個測試框架,也不是一次把功能做完的工作流。官方定位很明確:它是 reference(規則手冊),不是 driver(驅動程序)。真正跑循環的是你,或者同一倉庫裏的 implement Skill。
來源與歸屬:
- 作者:Matt Pocock(Total TypeScript / AI Hero)
- 倉庫:https://github.com/mattpocock/skills
- 目錄:
skills/engineering/tdd/ - 許可證:MIT
- 配套說明:https://www.aihero.dev/skills-tdd
- 分發頁:https://skills.sh/mattpocock/skills/tdd
SKILL.md 的 frontmatter 寫的觸發條件是:用戶要先測後寫地做功能或修 bug、提到 red-green-refactor、或者要寫集成測試。倉庫 README 對它的一句話是:按垂直切片做測試驅動開發,一次處理一個行爲。
它要解決的問題很具體:Agent 默認會「橫向切片」——先寫完全部測試,再寫全部實現。tdd 要求改成「縱向切片」:一個測試 → 一段剛好夠用的實現 → 再寫下一條。每一圈都是一發 tracer bullet(示蹤彈),用上一次循環學到的東西決定下一條測什麼。
核心規則¶
Skill 正文要求:下面每一節在每一圈紅綠循環裏都要看,不是做完再回頭補。
什麼算好測試¶
測試必須通過公開接口驗證行爲,而不是驗證內部結構。實現可以整段換掉,測試不該跟着動。好測試讀起來像規格,例如 "user can checkout with valid cart",一眼能看出系統具備什麼能力。
官方 tests.md 給的好例子是走真實調用路徑:
// GOOD: 測可觀察行爲
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
對照下面這條壞例子:它測的是內部協作方式,重構時行爲沒變,測試卻會紅。
// BAD: 測實現細節
test("checkout calls paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
Skill 還強調:期望值必須來自獨立的事實來源——規格里的字面量、手工算過的例子、需求本身。不能用實現同一套算法再算一遍當期望值。下面這條就是它點名的套套邏輯(tautological)測試:
// BAD: 期望值按代碼自己的方式重算,測了等於沒測
test("calculateTotal sums line items", () => {
const items = [{ price: 10 }, { price: 5 }];
const expected = items.reduce((sum, i) => sum + i.price, 0);
expect(calculateTotal(items)).toBe(expected);
});
// GOOD: 期望值是獨立的已知字面量
test("calculateTotal sums line items", () => {
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);
});
另外,不要繞過接口去查數據庫來證明「寫入成功」,而要用同一條公開接口把數據再讀回來:
// GOOD: 通過接口驗證
test("createUser makes user retrievable", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
好測試的特徵可以收成幾條:測調用方在意的行爲、只用公開 API、內部重構後仍能綠、描述 WHAT 而不是 HOW、一條測試一個邏輯斷言。
測在 seam 上¶
Skill 借用 Michael Feathers 的 seam(縫隙)這個詞:測試要落在公開邊界上,在那裏觀察行爲,不要伸進模塊內部。測試只寫在事先約定好的 seam 上。寫任何測試之前,Agent 必須先列出準備測的 seam,並跟你確認;未確認的 seam 不準寫測試。
它會問的問題是:「公開接口是什麼,我們應該在哪些 seam 上測?」
測不完所有邊角。事先約定 seam,是爲了把測試力氣花在關鍵路徑和複雜邏輯上。接口本身該多深、seam 該放哪、對外暴露什麼,這些詞來自同倉庫的 codebase-design Skill。官方說明寫得很清楚:tdd 在 v1.0 刪掉了自己那套深模塊筆記,改成引用這份共享詞彙表;codebase-design 需要一併安裝,但它是查閱用的參考,不是要另開一場設計會話。
探索代碼時,如果倉庫裏有 CONTEXT.md,要用裏面的領域用詞來寫測試名和接口名,並遵守相關 ADR。
三種反模式¶
SKILL.md 點名三類寫出來不值得留的測試:
- Implementation-coupled(綁死實現):mock 內部協作者、測私有方法、或者走旁路驗證(直接查庫而不是走接口)。判斷標準是:你只是重構,行爲沒變,測試卻紅了。
- Tautological(套套邏輯):斷言用代碼自己的方式重算期望值,例如
expect(add(a, b)).toBe(a + b),或者手工按實現推出來的 snapshot。這種測試天生不會跟代碼吵架。 - Horizontal slicing(橫向切片):先寫完全部測試,再寫全部實現。批量測試驗證的是想象中的行爲,測的是「東西長什麼樣」而不是用戶能做什麼,並且在還沒理解實現之前就把測試結構鎖死了。
正確做法是垂直切片:一個測試 → 一段實現 → 重複。每一條測試都是示蹤彈,根據上一圈學到的東西再寫下一圈。對照關係可以寫成:
WRONG(橫向):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT(縱向):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
循環規則¶
當前 SKILL.md 裏的循環只有兩步:
- Red before green. 先寫會失敗的測試,再寫剛好讓它通過的代碼。不要提前給後面的測試做準備,也不要加投機功能。
- One slice at a time. 每一圈:一個 seam、一個測試、一段最小實現。
- 重構不在這個循環裏。 重構歸同倉庫的
code-reviewSkill,在 review 階段做,不塞進紅綠實現循環。
這裏有一個需要單獨說清楚的地方。倉庫 README、skills.sh 摘要,以及 SKILL.md 的 description 字段,仍然寫着 red-green-refactor。作者在 aihero.dev 的技能說明裏解釋過:重構步驟在 2026 年 6 月被拿掉了,因爲 Agent 幾乎從不認真做這一步,而且實現和審查拆成兩次會話更合適;描述字段沒同步改掉,對應倉庫 issue #589。所以你說「red-green-refactor」仍然會觸發這個 Skill,實際跑的是紅 → 綠,重構交給 code-review。
什麼時候該 mock¶
配套文件 mocking.md 規定:只在系統邊界 mock。
可以 mock 的:
- 外部 API(支付、郵件等)
- 數據庫(有時可以,更推薦測試庫)
- 時間 / 隨機數
- 文件系統(有時可以)
不要 mock 的:你自己的類和模塊、內部協作者、任何你能控制的東西。
爲了讓邊界可 mock,官方給了兩條設計建議。
第一,依賴注入,不要在函數內部 new 出外部客戶端:
// 容易 mock
function processPayment(order, paymentClient) {
return paymentClient.charge(order.total);
}
// 很難 mock
function processPayment(order) {
const client = new StripeClient(process.env.STRIPE_KEY);
return client.charge(order.total);
}
第二,給每個外部操作單獨的函數,不要做一個帶一堆分支的通用 fetch:
// GOOD: 每個函數可以單獨 mock
const api = {
getUser: (id) => fetch(`/users/${id}`),
getOrders: (userId) => fetch(`/users/${userId}/orders`),
createOrder: (data) => fetch("/orders", { method: "POST", body: data }),
};
// BAD: mock 時要在內部寫條件
const api = {
fetch: (endpoint, options) => fetch(endpoint, options),
};
安裝與啓用¶
tdd 遵循通用的 SKILL.md 格式,Cursor、Codex CLI、Claude Code 等支持 Agent Skills 的工具都可以用。官方提供兩條安裝路線,README 寫明二選一,兩套都裝會得到兩份重複的 Skill。
倉庫目錄大致是:
skills/engineering/tdd/
SKILL.md
tests.md
mocking.md
agents/openai.yaml
tests.md 和 mocking.md 是循環中按需查閱的參考,不是可執行腳本。
1. 只裝 tdd(以及它依賴的 codebase-design)¶
skills.sh 上這個 Skill 的安裝命令是:
npx skills add https://github.com/mattpocock/skills --skill tdd
作者在 aihero.dev 寫過:tdd 在 v1.0 之後依賴 codebase-design 提供 seam / 深模塊那套詞彙,需要一併安裝。tdd 本身是無狀態的,不會往倉庫裏寫文件。
2. 用 skills.sh 裝整套,再按需勾選¶
這是 README 給 Codex 以及其他 Agent 的默認方式,Cursor 也可以用。安裝器會讓你選擇 Skill 和目標 Agent,並把文件寫進倉庫,之後可以改:
npx skills@latest add mattpocock/skills
勾選時把 tdd 帶上。如果還要用同一套工程技能(拆票、實現、審查),README 要求同時勾選 setup-matt-pocock-skills,裝完後在 Agent 裏跑一次 /setup-matt-pocock-skills,配置 issue 跟蹤器、triage 標籤和文檔存放位置。只單獨用 tdd 做紅綠循環,不必走完這套倉庫配置。
更新已拷貝到本地的文件:
npx skills update
3. Claude Code 插件(整套只讀、跟隨上游更新)¶
當前 README 寫的是:這套 Skill 已進入 Claude Code 官方 marketplace,不必先加源。
claude plugins install mattpocock-skills
會話裏也可以:
/plugin install mattpocock-skills
插件裝的是整套只讀包,會隨作者發佈更新。不要和 skills.sh 那條路線混裝。
Cursor 如何發現它¶
Cursor 會從下面這些目錄自動加載 Skill:
| 位置 | 範圍 |
|---|---|
.agents/skills/、.cursor/skills/ |
當前項目 |
~/.agents/skills/、~/.cursor/skills/ |
當前用戶全局 |
.claude/skills/、.codex/skills/ 以及對應的家目錄 |
兼容 Claude Code / Codex |
每個 Skill 是一個包含 SKILL.md 的文件夾。npx skills 會按你勾選的 Agent 把文件寫到對應目錄。也可以手動把官方目錄拷到項目裏,例如 .cursor/skills/tdd/SKILL.md。在 Cursor 的 Agent 對話框輸入 /,搜 tdd 即可手動調用。
典型用法¶
單獨調用¶
有一個已經說得清的行爲——有輸入、有可觀察輸出——直接 /tdd。也可以在對話裏寫「先寫失敗測試再實現」「按 TDD 做」「要集成測試」,讓 Agent 按 description 自己選中它。
官方期望你看到的過程是:
- Agent 先列出準備測試的公開 seam,停下來等你確認。沒有確認之前不寫測試文件。
- 寫一條會失敗的測試,確認它是因爲行爲還不存在而紅,不是測試本身寫錯。
- 只寫剛好讓這一條通過的實現。
- 再寫下一條。不要一次丟進一批測試。
第一條測試就是示蹤彈:先證明有一條端到端路徑是通的,再往外長。
放進完整工程鏈¶
同一倉庫裏,tdd 是構建步驟內部的引擎,不是單獨的「做完所有事」步驟。主鏈路在官方文檔裏寫成:
grill-with-docs → to-spec → to-tickets → implement → code-review
含義是:to-spec 先約定測試 seam;implement 按工單驅動 tdd;code-review 檢查是否只在約定過的 seam 上寫了測試,並承擔 tdd 不再做的重構。手頭已經有 spec 或工單、想一次跑完構建時,官方建議跑 /implement,而不是單獨拿 /tdd 當整段工作流。
沒有完整 spec、只想先測後寫某一個具體行爲時,直接 /tdd 即可。
怎樣算它在工作¶
官方給出的驗收信號包括:
- 任何測試文件出現之前,它會停下來報 seam 並等待。
- 一次只出現一條測試,先紅再綠,然後才寫下一條;不是一批測試配一批代碼。
- 測試名讀起來像能力(
user can checkout with valid cart),不像內部步驟(checkout calls paymentService.process)。 - 斷言裏的期望值能追溯到規格或已知例子,不是按實現重算出來的。
- 給內部函數改名,測試套件不該跟着紅。
- mock 只出現在外部邊界(支付 API、時鐘),不包你自己的模塊。
適用場景與注意事項¶
適合用 tdd 的情況:行爲已經釘住,有明確輸入和可觀察輸出。官方舉例包括業務邏輯、請求/響應契約、數據變換、校驗。
不適合、或者說官方自己標成缺口的情況:配置、接線、膠水代碼、純類型標註、直接把 CRUD 轉交給下層。這類改動往往沒有獨立的事實來源可以斷言,硬跑循環容易寫出 Skill 自己警告過的套套邏輯測試。這件事對應倉庫 issue #746,文檔寫明在關閉之前,要不要走 TDD 由你或倉庫的 CLAUDE.md 決定。
另外幾條官方已經記錄的限制,用的時候值得提前知道:
- Agent 仍可能先寫實現。 技能說明裏寫過:有人追問模型爲什麼沒先寫測試,得到的回答是「我讀了規則,但還是回到了平常的習慣」。Skill 不會 100% 強制執行。某一刀必須嚴格紅先於綠時,需要盯着這次運行,而不是假設文件在就能保證紀律。
- 不要默認從瀏覽器 / E2E 測起。 有用戶遇到 Agent 先寫 Playwright 測試,再長時間循環,最後判斷是測試壞了——而功能當時還不存在。瀏覽器測試太慢,紅綠反饋會虧本。官方建議在
CLAUDE.md裏寫明:行爲先在更快的測試裏做穩,再補瀏覽器測試。 - 選 seam 會卡住。 這是反饋最多的摩擦點(issue #607)。提示往往只給候選 seam 的名字,不說明各測到什麼、漏掉什麼。實用做法是先讓 Agent 講清楚:組件級 seam 漏什麼、集成級 seam 慢多少,再做選擇。完整鏈路裏這件事會提前在
to-spec裏定掉。 - 它看不到其他工單。 對着一張票跑時,它可能提出屬於兄弟工單的工作(issue #129)。作者的立場是這不是
tdd的職責。把 spec 和工單一併給它,或者把工單本身切到合適的大小,更有效。 /tdd不替代/implement。 課程裏原來的/do-work現在拆成/implement、/tdd、/code-review。對着一張票該跑哪一個,官方几乎總是回答/implement。
小結¶
tdd 做的事情很窄:把 Agent 從「先鋪完全部測試再填實現」拉回「一條測試、一段實現、再下一條」。測試落在事先約定的公開 seam 上,斷言對着可觀察行爲,mock 停在系統邊界。重構不在這個循環裏,接口深度那套詞也不在這個文件裏,分別交給 code-review 和 codebase-design。
它管不住模型偶爾偷跑去先寫代碼,也決定不了「這個改動值不值得測」。能做的是:當你已經知道要驗證哪條行爲時,給 Agent 一份每圈都要查閱的規則,讓留下來的測試更像規格,而不是實現的影子。
官方地址:
- Skill 目錄:https://github.com/mattpocock/skills/tree/main/skills/engineering/tdd
- 技能說明:https://www.aihero.dev/skills-tdd
- 安裝頁:https://skills.sh/mattpocock/skills/tdd
- 倉庫:https://github.com/mattpocock/skills