為什麼 API 自動化總是卡在同樣的四個地方
每個團隊的起點都一樣。有人針對單一端點寫了一個請求,斷言狀態碼,然後把它放進 CI。它會動。接著測試套件開始變大,四個問題幾乎會以同樣的順序找上每一個人。
權杖會過期,於是套件早上還會通過,到了午餐時間就失敗。回應裡包含下一個請求需要的值,於是測試不是把它們寫死,就是壞掉。有些端點要等別的東西先被建立出來才能呼叫,於是順序開始變得重要,而沒有人把順序寫下來。而且每一次執行都會留下資料列,於是第十次執行的行為和第一次不一樣。
這些都不是什麼罕見狀況,而是真實後端本來的樣子。一套能長久存活的測試,和一套用了一季就被刪掉的測試,差別只在於這四件事是一開始就被設計進去,還是後來才被發現。
從服務實際開放的介面開始
在那之前,你需要一份清單。憑印象寫測試,得到的只會是一套涵蓋你剛好想得到的那些端點的測試,而那些通常都是本來就沒問題的端點。
有兩種輸入能讓你看到真實的樣貌。規格文件,例如 OpenAPI 檔案或產品需求文件,告訴你「應該存在」什麼;對執行中的服務做一次探索,則告訴你「實際存在」什麼。兩者之間的落差本身就很值得一看,而且比起從一張白紙開始,它是決定涵蓋範圍更好的起點。在 TestSprite 裡,這第一步叫做 API Discovery,它接受 PRD、API 文件,或是對實際呼叫的觀察。
接下來,你要挑出值得涵蓋的端點,而不是照單全收。一份把所有管理用和內部路由都納入的產生計畫就是雜訊,而測試套件失去讀者的方式,正是雜訊。
問題一:身分驗證(Auto-Authentication)
最直覺的做法是把權杖放進環境變數,等到有人發現測試失敗時再手動更新。這招一直有用,直到知道這件事的人去度假。
你真正想要的,是讓憑證屬於環境而不是屬於某一個測試,並且把登入變成執行流程的一部分。TestSprite 把這件事叫做 Auto-Authentication:登入流程只要設定一次,每次執行前都會取得一個全新的權杖。有兩個原則,比聽起來更重要。
把憑證和測試分開。 一個內含密碼的測試沒辦法分享、沒辦法指向第二個環境,也沒辦法提交進版本庫。測試案例應該說「以已登入的使用者身分」,而不是「以 user@example.com 搭配這組密碼」。
在服務允許的情況下,每次執行只驗證一次。 任何有登入速率限制的服務,都會開始拒絕一套每個案例都重新登入的測試,而這些拒絕看起來就跟產品本身的錯誤一模一樣。診斷這類假失敗所花的時間,比一開始就繞開它還多。
問題二:只在執行期才存在的值(Dynamic Variables)
建立資源的呼叫會回傳一個識別碼,而更新和刪除的呼叫需要它。如果這個識別碼是直接貼進測試裡的,那這個測試就只有效一次。
解法是從回應中擷取這個值,之後再引用它,讓測試套件把資料往下帶,而不是憑空假設。實務上,這表示一個測試不會寫成 DELETE /orders/8412。它說的是:刪除這次執行所建立的那筆訂單,不管它的識別碼最後是什麼。
寫下來看似理所當然,但這正是一套上個月還會通過的測試今天失敗的最常見原因。這個機制在 TestSprite 裡有個名字,叫做 Dynamic Variables:從一個回應中擷取值,再注入到下一個請求,而其中的串接是自動推導出來的,不需要手動維護。
問題三:彼此相依的端點(Dependency Chains)
你沒辦法把商品加進一個不存在的購物車,也沒辦法把一個空的購物車拿去結帳。真正的 API 涵蓋範圍不是一張扁平的呼叫清單,而是一張圖。
當這張圖只是隱含存在時,有兩件事會出錯。測試執行的順序在某一台機器上剛好可行,在另一台上卻不行;而前面的一個失敗會製造出二十個下游失敗,每一個看起來都像是獨立的錯誤。
把相依關係明確寫出來,兩個問題就都解決了。每個案例宣告自己需要什麼、產出什麼,執行時就能據此推算出一個安全的順序;出問題時,你看到的是一個失敗加十九個略過,而不是二十列要一一讀過的紅字。這就是 Endpoint Tests 和 Integration Tests 的差別:前者一次涵蓋一條路由,後者則是多步驟的串接,其中一個回應會餵給下一個。
問題四:你留下來的資料(Auto-Cleanup)
一套只建立記錄、從不刪除的測試,會照著自己的節奏逐漸劣化。清單愈來愈長,分頁的斷言開始飄移,唯一性約束開始被觸發,最後某個星期五會有人手動清空一張資料表。
清理必須是執行流程的一部分,而且要綁定在那一次執行所建立的資源上。用樣式比對來清理,例如刪掉所有名稱開頭是 test-的東西,這招在兩次執行重疊、或業務端某個人剛好建了一筆名字不巧的記錄之前,都還有用。Auto-Cleanup 會在執行結束時刪掉這次執行所建立的記錄,這才是能在並行環境下存活的版本。
一套能運作的 API 自動化,從頭到尾長什麼樣子
把這四件事放在一起,一套能撐過一年的設定會是這樣。
| 一開始做一次 | 把規格交給專案,讓探索流程列舉出線上實際存在的端點。挑出值得涵蓋的端點。忍住第一天就想要完整涵蓋的衝動。以環境為單位設定憑證,而不是以測試為單位。 |
| 每個案例 | 擷取下一步需要的值,而不是寫死。宣告這個案例需要什麼、產出什麼。針對行為做斷言,而不只是狀態碼。回傳 200 但內容是錯的,一樣是錯誤。 |
| 每次執行 | 驗證一次,並沿用同一個工作階段。讓相依關係圖決定執行順序。執行結束時,精準刪掉這次執行所建立的東西。 |
把它放進 CI 流程
一套要靠有人想起來才會跑的 API 測試,等於是在發版前才跑、而不是在合併前跑,而這是錯誤的那一端。接進流程有兩種方式,而選擇的重點在於你希望由誰來擁有這個觸發點。
在選擇之前值得先知道:有兩種介面,而它們解決這件事的方式不同。網頁版是「產生並維護」的路線,探索、相依串接和清理都會幫你推導出來。CLI 則是「自備程式碼」的路線:後端測試是你自己寫的 Python,每個測試需要什麼、產出什麼由你自己宣告,而收尾清理則是一個你標記出來的測試。同樣的四個問題,不同的分工。希望測試套件住在自己版本庫裡的團隊,會刻意選擇後者。
兩條路線,而選擇的重點在於你希望設定放在哪裡。
兩條路線,而選擇的重點其實在於這條流程由誰擁有。從儀表板連接 GitHub App 完全不需要更動你的版本庫,因為它是對你的建置本來就會產出的部署做出反應。加上一個 GitHub Actions 步驟,則是把這道檢查放進版本庫裡,讓它像建置流程的其他部分一樣被審查;目前的指令放在 CLI 版本庫。
兩個誠實的限制
這是功能性驗證。產生的計畫確實包含邊界值和授權相關的案例,所以一次執行會告訴你超出範圍的值是否被拒絕、未經驗證的請求是否被擋下。但這和負載測試或弱點掃描不是同一回事。它不會告訴你服務在持續並行壓力下的表現,它也不是掃描器。那些是另外的工具,假裝不是這樣只會浪費你一個下午。
它也救不了一個沒有規格、也沒有一致慣例的 API。探索可以列舉出端點,但「正確」對每個端點而言代表什麼,仍然是一個判斷,而這個判斷得由你團隊裡的某個人來下。這份工作比手寫整套測試少得多,但不是零。
TestSprite 如何處理這四個問題
這篇文章裡的四個失敗點,不是 API 測試的偶發狀況,所以它們是被當成產品功能來處理,而不是丟給你自己的基礎設施。Auto-Authentication 只需要設定一次登入流程,之後每次執行前都會取得新的權杖。Dynamic Variables 會把一個回應裡的值帶進下一個請求。Dependency Chains 會依據每個案例需要什麼、產出什麼,推導出安全的執行順序。Auto-Cleanup 會精準刪掉這次執行所建立的記錄,這才是能在並行執行下存活的版本。
涵蓋範圍的來源也不一樣。API Discovery 會列舉出執行中的服務實際開放了什麼,plan generation 則會在功能、結構描述、授權、錯誤處理和邊界這幾個類別下提出案例,而你可以用自然語言來調整它們。產出的結果裡,包含了那些在衝刺尾聲沒有人會寫的反向案例。
這樣下來的結果是:一套會隨著 API 改變而持續運作的測試,而且不需要一個只有某一個人看得懂的 conftest 或支援目錄。負載容量和弱點掃描,仍然是另外的工具。
一開始就需要 OpenAPI 規格嗎?
有幫助,但不是必要。一份產品需求文件,加上對執行中的服務做一次探索,就足以得到一份可用的計畫。規格主要是提升第一批產生案例的品質。
這和一堆存起來的請求集合有什麼不同?
集合儲存的是請求。這篇文章裡的四個問題,講的都是請求之間的狀態:工作階段、擷取到的值、執行順序和清理。只要寫夠多腳本,集合也能處理這些,而那正是大多數團隊想要避開的維護負擔。
它能測試無法從公開網路存取的 API 嗎?
先確認你說的是哪一端。CLI 可以建立通道連到跑在你自己機器上的應用程式,所以 localhost 上的前端是涵蓋得到的。但這個本機模式只支援前端,因此一個無法從網際網路存取的後端服務,在執行 API 測試時仍然需要一個可連線的 URL。在規劃上線之前,值得先對照你的預備環境確認一下。
那些需要多因素驗證登入才能存取的 API 呢?
使用一個沒有第二道驗證、而且僅限於測試環境的專用測試帳號。把真正的第二道驗證自動化是做得到的,但幾乎總是脆弱到不值得。
第一套測試應該涵蓋多少端點?
涵蓋那些讓你在發版前會緊張的流程,而不是最好寫的端點。十個每次 pull request 都會跑的有意義案例,勝過兩百個沒有人會讀的案例。
如果你想看的是運作機制而不是背後的理由,這四件事各自在 API 測試文件 中都有專屬的頁面。