程式開發

tdd

以公開介面與事先確認的 seam 進行垂直切片測試,每輪先寫一個會失敗的行為測試,再加入最小實作通過,避免內部耦合、同義反覆與橫向批次測試。

查看來源 ↗
作者/來源mattpocock★ 244,180 GitHub Stars
測試版本6654f6b60cd9更新日期:2026-08-24T14:19:57Z
支援平台OpenAI · Claude本次實測平台
安裝方式npx skills add mattpocock/skills --skill tdd
公平比較方式

三個問題,分別測載入前與載入後。

每次回答都在全新環境中產生。兩邊使用相同模型與設定,唯一差別是右側載入了這個 Skill。

模型版本gpt-5.6-sol帳號方案ChatGPT Plus思考強度medium執行工具codex-cli-native-skill
真實測試使用本機已登入的 ChatGPT Plus 實際執行,完成於 2026年8月28日。所有檢查通過後才公開,回答未經人工改寫。
三階段測試3 個問題全部完成版本核對3/3 通過環境清理9/9 通過測試隔離每回合全新環境
01
基礎題

運費規則第一個切片

測試問題

我們已確認公開 seam 是 calculateShipping(orderTotal: number): number,使用 TypeScript + Vitest。規格第一條:訂單金額 1,000 元以上免運;其餘規則尚未決定。請只完成第一個 TDD 垂直切片:依序提供 RED 測試、為何會失敗、GREEN 最小實作與通過後狀態;不得先實作未決定的規則,也不要聲稱已執行測試。

載入前 · 0 個 Skill載入後 · 版本已核對再次確認 · 0 個 Skill已確認模型套用 Skill三個環境皆已清理
未載入 Skill306 tokens · 10993 ms
### RED:先寫失敗測試 ```ts // calculateShipping.test.ts import { describe, expect, it } from 'vitest'; import { calculateShipping } from './calculateShipping'; describe('calculateShipping', () => { it('訂單金額達 1,000 元時免運', () => { expect(calculateShipping(1_000)).toBe(0); }); }); ``` ### 為何會失敗 目前尚未提供 `calculateShipping` 實作,因此測試會因模組或匯出函式不存在而失敗。這是預期的 RED 狀態。 ### GREEN:最小實作 ```ts // calculateShipping.ts export function calculateShipping(orderTotal: number): number { if (orderTotal >= 1_000) { return 0; } throw new Error('訂單未滿 1,000 元的運費規則尚未決定'); } ``` ### 通過後狀態 在 `orderTotal === 1_000` 的測試案例中,函式回傳 `0`,因此此測試應可通過。未滿 1,000 元的行為仍明確標示為未決定,沒有預先實作其他規則。以上僅提供 TDD 垂直切片,未實際執行測試。
已載入 Skill +1216 tokens · 30654 ms
### RED 測試 ```ts // calculateShipping.test.ts import { describe, expect, it } from 'vitest'; import { calculateShipping } from './calculateShipping'; describe('calculateShipping', () => { it('訂單金額達 1,000 元時免運', () => { expect(calculateShipping(1_000)).toBe(0); }); }); ``` ### 為何會失敗 此時 `calculateShipping` 尚未實作或匯出,因此測試無法載入函式,形成 RED。 ### GREEN 最小實作 ```ts // calculateShipping.ts export function calculateShipping(orderTotal: number): number { if (orderTotal >= 1_000) { return 0; } throw new Error('尚未定義 1,000 元以下的運費規則'); } ``` 低於 1,000 元時明確標示規則尚未定義,沒有臆測運費。 ### 通過後狀態 加入最小實作後,這個測試預期會通過:`calculateShipping(1_000)` 回傳 `0`。未執行測試,因此不宣稱實際測試結果。
02
實務題

修正重試次數錯誤

測試問題

已確認公開 seam 為 async retry<T>(operation: () => Promise<T>, maxAttempts: number): Promise<T>。目前 bug:maxAttempts=2 時會呼叫 operation 三次。測試框架 Vitest。請用一個 red→green 切片修正,測試只能經公開 retry 觀察行為,可用受控 fake operation,但不要 mock 內部 helper;提供測試先於最小修正,且不得聲稱已執行。

載入前 · 0 個 Skill載入後 · 版本已核對再次確認 · 0 個 Skill已確認模型套用 Skill三個環境皆已清理
未載入 Skill484 tokens · 13898 ms
```ts // retry.test.ts — Red import { describe, expect, it, vi } from "vitest"; import { retry } from "./retry"; describe("retry", () => { it("最多呼叫 operation maxAttempts 次", async () => { const error = new Error("失敗"); const operation = vi.fn<() => Promise<never>>().mockRejectedValue(error); await expect(retry(operation, 2)).rejects.toBe(error); expect(operation).toHaveBeenCalledTimes(2); }); }); ``` ```ts // retry.ts — Green(將原本多執行一次的 `<=` 改為 `<`) export async function retry<T>( operation: () => Promise<T>, maxAttempts: number, ): Promise<T> { let attempts = 0; while (attempts < maxAttempts) { attempts += 1; try { return await operation(); } catch (error) { if (attempts === maxAttempts) { throw error; } } } throw new Error("maxAttempts must be greater than 0"); } ```
已載入 Skill +1439 tokens · 41956 ms
```ts // retry.test.ts — RED import { describe, expect, it } from "vitest"; import { retry } from "./retry"; describe("retry", () => { it("最多呼叫 operation maxAttempts 次", async () => { let attempts = 0; const failure = new Error("failed"); const operation = async (): Promise<never> => { attempts += 1; throw failure; }; await expect(retry(operation, 2)).rejects.toBe(failure); expect(attempts).toBe(2); }); }); ``` ```diff // retry.ts — GREEN(最小修正) - for (let attempt = 0; attempt <= maxAttempts; attempt++) { + for (let attempt = 0; attempt < maxAttempts; attempt++) { ``` 以上未執行;修正假設目前多呼叫一次源自該迴圈的 `<=` 邊界。
03
壓力題

拒絕橫向批次測試

測試問題

產品要求一次先寫完結帳的 18 個測試,再開始實作。已確認第一個 seam 是 checkout(cart, paymentGateway),第一個能力是空購物車回傳 EMPTY_CART 且不得呼叫 paymentGateway;折扣、庫存與付款失敗規格仍未確認。請說明你會如何回應,並只給第一個 tracer-bullet 的 Vitest 測試與最小實作;不要把未確認規格寫成測試。

載入前 · 0 個 Skill載入後 · 版本已核對再次確認 · 0 個 Skill已確認模型套用 Skill三個環境皆已清理
未載入 Skill393 tokens · 12449 ms
我會先指出:目前無法負責任地一次寫完 18 個測試,因為折扣、庫存與付款失敗規格尚未確認;把猜測寫成測試會錯誤地固化需求。我會保留「先測試、後實作」原則,但只提交已確認能力的第一個 tracer bullet:先寫並確認測試失敗,再加入最小實作。其餘測試等待規格確認。 ```ts // checkout.test.ts import { describe, expect, it, vi } from 'vitest' import { checkout } from './checkout' describe('checkout', () => { it('returns EMPTY_CART without calling paymentGateway for an empty cart', () => { const paymentGateway = vi.fn() const result = checkout([], paymentGateway) expect(result).toBe('EMPTY_CART') expect(paymentGateway).not.toHaveBeenCalled() }) }) ``` ```ts // checkout.ts export function checkout( cart: unknown[], paymentGateway: (...args: unknown[]) => unknown, ): 'EMPTY_CART' { if (cart.length === 0) { return 'EMPTY_CART' } throw new Error('Checkout behavior for non-empty carts is not specified') } ```
已載入 Skill +1010 tokens · 33597 ms
我會回應:目前只能合法寫出 1 個已確認行為;先寫 18 個測試會迫使團隊臆測其餘規格。建議以 tracer bullet 逐條完成「測試失敗 → 最小實作通過」,待折扣、庫存與付款失敗規格確認後再補其餘測試。以下暫以 `[]` 表示空購物車;若既有 `Cart` 模型不同,只替換測試資料。 ```ts // checkout.test.ts import { describe, expect, it, vi } from 'vitest' import { checkout } from './checkout' describe('checkout', () => { it('returns EMPTY_CART without calling the payment gateway for an empty cart', () => { const paymentGateway = vi.fn() const result = checkout([], paymentGateway) expect(result).toBe('EMPTY_CART') expect(paymentGateway).not.toHaveBeenCalled() }) }) ``` ```ts // checkout.ts export function checkout( cart: unknown[], paymentGateway: unknown, ): 'EMPTY_CART' { return 'EMPTY_CART' } ```
測試紀錄

這次測試記錄了什麼?

固定不變兩邊使用相同的模型版本、帳號方案、系統指令、思考強度與測試問題。

分開測試每次回答都使用全新環境。載入前不含任何 Skill,載入後只含本頁指定的 Skill。

完整才公開三題的所有步驟與環境清理都成功後,才顯示結果。若無法確認模型是否套用 Skill,也會清楚標示。