iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Claude AI

盡信 Claude,不如無 Code — 心法與全端實戰系列 第 23 篇

Day 23 報名 API:先定好標準答案,再讓 AI 寫實作

  • 分享至 

  • xImage
  •  

昨天 schema 定案,今天在它上面立 API。原本規劃 17 條:認證 4、活動 5、票種 2、保留與確認 3、訂單 3。17 條做完之後,我又加了第 18 條,後面會講。

但今天真正的題目不是「API 通了」,是契約跟實作誰先寫。

這裡的「契約」指 openapi.yaml:一份寫明每條 API 收什麼、回什麼的規格檔。後端照它實作,iOS 用它產生呼叫 API 的程式碼 —— 可以把它想成標準答案。契約測試則是拿這份標準答案,每條 API 實際打一次,檢查回來的 status 和 body 是否符合契約。


契約和契約測試,都比實作早

這個順序決定了契約測試有沒有獨立裁判的資格。

先實作再補契約,你會得到一份照著實作寫出來的契約 —— 它跟實作永遠一致,因為它是從實作抄來的。這種測試發現不了什麼,只會確認「程式碼等於程式碼」。

所以順序是:

1. 寫 openapi.yaml,commit
2. 寫契約測試,commit
3. 讓 AI 照契約實作,契約測試對實際回應斷言

這一次第 1 步有個轉折:openapi.yaml 不是我手寫的,是 Day 21 spec-kit 從我的規格產的。它的獨立裁判資格不來自「誰寫的」,而是來自兩件事:它從規格來、不從實作來;它的 commit 早於實作,這點讀者可以自己查:

(在這裡插入圖片:day23-contract-timeline.png)
https://ithelp.ithome.com.tw/upload/images/20261007/20103790SiqC8WB7xU.jpg

bca4d9a → 238ea84 → a1b34aa → 784f2d1 → 73818c3

契約測試有一個設計我很喜歡:骨架階段所有 handler 都回 501,而契約裡沒有 501 這個狀態碼 —— 但 not_implemented 在錯誤代碼的列舉裡。所以測試規定 501 只准以契約的 Error 形狀出現。每接好一條,那條就自動從「驗 501 的形狀」變成「驗真正的回應」,不用改測試。

npm run test:contract
# Tests  20 passed (20)

20 個:先確認契約裡剛好 19 個 operation(18 條 endpoint 加 /health),再每條各打一次。

第 18 條,契約跟實作一起寫

17 條做完之後,我在 web 的保留頁想要一個「套用」按鈕:輸入優惠碼,先看折完多少、這個碼划不划算,再決定要不要確認。這需要一條新的 endpoint:POST /holds/:id/quote,試算、不建單、不用掉碼。

它的紅測試先 commit 了(f544ca2),但契約跟實作是同一個 commit(04666ae)。對這一條來說,契約測試還是能驗回應的形狀,但它失去了獨立性 —— 契約是跟著實作一起寫出來的,它們當然一致。

而那個 commit 的 message 最後一句寫著「iOS client 重產通過」。下一個 commit 是這樣開頭的:

248d7aa  契約修正:confirm 200 的 description 含半形逗號,在 YAML flow mapping 裡
         被當成分隔,Swift 產生器報錯 …… 更正上一個 commit 說的
         「iOS client 重產通過」,那時其實沒過

契約跟實作擠進同一個 commit,同一個 commit 裡就冒出一句沒跑過的宣稱。這讓我看到:順序一亂,「先有裁判再動手」那道習慣也跟著鬆了。 我把這一段留在文章裡,因為這個系列的第一條規矩就是「跑過才算數」,而這次沒守住它的不是 AI 的第一版,是趕進度的那一刻。

契約裡最值錢的三個欄位

契約不只是路徑跟參數。這份契約裡有三個欄位,是刻意放進去讓測試看得到的:

欄位 在哪 為什麼
server_now 每個 JSON 回應(另有 x-server-now header) 客戶端不需要、也不應該用自己的時鐘判斷截止
remaining_seats GET /events 的每個活動 併發測試的不變條件靠它:這次測試每個成功請求只拿一席,所以搶完之後,成功筆數 + remaining 必須等於開始時的名額
expires_at POST /events/:id/holds 回的 hold TTL 的邊界要能從外面觀察,否則測不到

實際打一次(本機 wrangler dev):

GET /events
x-server-now: 1790509510434
{"events":[{"id":"ev-1","name":"秋季音樂會","status":"on_sale",…,"remaining_seats":100}],"server_now":1790509510434}

POST /events/ev-1/holds
x-server-now: 1790509510445
{"id":"1a9bc103-…","seat_nos":["J9"],"status":"holding",
 "expires_at":1790510110445,"server_now":1790509510445}

expires_at − server_now = 600000,剛好十分鐘。前端的倒數就拿這兩個數字相減,不碰手機的時鐘。

第一個欄位特別重要。只要回應裡有 server_now,前端就沒有藉口拿自己的時鐘判斷還能不能報名(倒數動畫可以跑,但「到了沒」要以伺服器的時間為準) —— 這是把一條規則寫進契約,而不是寫進文件。

狀態和時間:兩個真相來源

規格裡有一條寫得很不起眼,但它是這 18 條 endpoint 裡最容易只做一半的地方。大意是:

報名要同時檢查兩件事:活動的 status 是 on_sale,而且 opens_at <= now < deadline_at。

status 欄位和時間欄位是兩個真相來源,而它們可以不一致 —— 時間到了但狀態還沒被任何人更新,是可能發生的情況,不是理論上的例外(誰去更新?排程?第一個請求?)。

所以測試分開寫,兩條都在 tests/routes/holds.test.js:

it('closed 的活動 → 409 not_on_sale', ...)                     // 狀態關了,時間還沒到
it('status 仍是 on_sale,但時間過了 deadline_at → 409', ...)    // 時間到了,狀態還開著

只寫一個的話,AI 只實作其中一半也會綠。跟 Day 9 那個練習是同一個道理,只是換了個地方發生。

測試看不到的三個地方

原本預期會卡的是 D1 綁定、Hono 中介層順序、測試資料從哪來 —— 三個都沒卡。真正卡住的三件,說穿了是同一件事:測試看得到的世界,比系統真實的世界小。 一個是另一個契約消費者,一個是請求端的規則,一個是測試自己的前置條件。

一、契約測試讀得懂,Swift 讀不懂。 就是上面那個逗號(它在 12:01 就寫進契約了,重產 iOS client 時才炸出來)。契約裡一段 description 含半形逗號,寫在 YAML 的 { … } 行內寫法裡,被當成欄位分隔。契約測試用的 YAML 解析器(npm 的 yaml 套件)讀得進去,所以契約測試全綠;iOS 那邊的 swift-openapi-generator 讀不進去,直接報錯。同一份契約有兩個讀者,只有比較嚴格的那個會替你抓格式錯誤。

二、契約測試只驗回應,不驗請求。 兩件事都是事後 spec-kit 重跑時才抓到的(一件在 converge,一件在 clarify):

  • 登出要帶 refresh token 來撤銷它,所以契約說 logout 的 body 是必填;實作不帶 body 照樣回 204(990a3bc 改成 400)。
  • 契約說一次最多保留 100 席,程式早就寫死 10 席(39c800e 把契約改成 10)。

兩件都通過了契約測試,因為測試沒有拿請求本身去對契約:有 body 的一律送 {},只檢查回應長得對不對。契約裡的請求限制跟程式碼的限制對不對得上,契約測試驗不到。

三、測試自己寫錯。 「過了截止不能保留」那條測試把假時鐘往後推一天,結果回的是 401 不是 409 —— 因為 access token 只有 15 分鐘,推一天之後登入早就過期了。那次 401 不是保留邏輯出錯,而是測試的前置條件先失效了。現在那行測試旁邊留著一句註解:「推了一天,token 早過期,重新登入」。


誰當裁判

這一篇的裁判,就是這一行:

npm run test:contract    # OpenAPI schema 對實際回應斷言

這一篇留下的心法:

契約測試的獨立裁判資格,來自它比實作早存在 —— 實作寫完才補的契約,可能跟著實作一起錯。
照著程式寫出來的契約,錯的地方也會一起照抄 —— 程式有 bug,契約跟著有,測試照樣綠。

明天:登入。一個只拿到一句需求的 AI 寫 JWT,六個測試加一條靜態規則打它。


參考資料


上一篇
Day 22 讓 AI 盲做 schema:哪些差異真的該寫進規格?
下一篇
Day 24 AI 寫的 JWT 六關過四關:紅燈不在 JWT,問題在時間從哪來
系列文
盡信 Claude,不如無 Code — 心法與全端實戰 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言