GraphQL 合約測試
概述
位於層級之間接縫上的測試,當任一方未經對方同意而移動時會失效——tests-as-a-control 應用於邊界。核心原則:針對合約——消費者依賴的資料形狀與存取語意——進行斷言,絕不針對查詢字串本身。 因為無關緊要的查詢編輯而失敗的測試是一種變更檢測反模式;僅在保證的形狀或權限移動時才失敗的測試才是控制。
§1 — GraphQL 查詢合約
- 每個操作都以類型化文件的形式在已知位置被撰寫一次,並被匯出以供匯入。 UI 匯入它;測試匯入完全相同的構件——絕非重新輸入的副本。將查詢複製到測試中會無聲地破壞合約(編輯 UI 查詢不再使測試失敗)。DRY 原則變得至關重要。
- 它天生在兩個方向上都會失敗:
- 資料庫 → UI 偏移,在編譯時期。
graphql-codegen從即時 schema 產生 TypeScript 型別。重新命名或刪除一個被使用的欄位,tsc會在每個使用點以及共享文件上失敗——在使用者看到空白面板之前,建置就會中斷。這就是為什麼必須針對即時 schema 進行程式碼生成。 - UI → 資料庫 / 權限偏移,在執行時期。 共享文件透過實際呼叫路徑,在角色上下文中對已注入資料的測試資料庫執行,並針對回傳的形狀與存取結果進行斷言——而非查詢文字。
- 資料庫 → UI 偏移,在編譯時期。
- 合約測試與 RLS 角色矩陣是同一套架構。 將同一份文件以角色 A(預期取得資料)和角色 B(預期被拒絕/為空)身分透過 PostGraphile 執行。一個機制驗證三件事:查詢仍符合 schema、UI 假設仍成立、RLS 仍強制執行邊界。
實際範例——真實 bug 如何被捕捉
- 資料庫→UI: 一次遷移移除了
equipment.is_active。Codegen 從新 schema 重新生成;生成的型別失去該欄位;tsc在共享文件和所有使用它的元件上失敗——在建置階段、合併前就被捕捉。 沒有測試斷言「查詢文字 == X」。 - UI→資料庫: 開發者擴展了 UI 查詢以拉取
cancellation_reason(RLS 僅向較高等級角色暴露該欄位)。以普通成員身分執行的執行時期測試斷言了成員可見的形狀,現在得到 null/被拒絕,並失敗——強迫思考「我們有意暴露這個嗎?」
§2 — 路由合約與煙霧測試
- 從單一來源列舉路由。 每個路由都有一個煙霧測試:渲染無錯誤 + 顯示一個最小必要元素集合(一個標題、一個地標、一個關鍵控制項——而非詳細互動)。
- 新增/移除路由會強制對應的測試變更——沒有煙霧測試的新 URL 是不完整的;已移除的 URL 會移除其測試。URL 絕不會沉默地出現或消失。
- 明確來說不是詳細的 UI/互動測試——有意識地、範圍有限。問題是「這個 URL 是否能夠運作並渲染基本要素」,僅此而已。
強制執行: CI 中使用 graphql-codegen + tsc(阻斷式)處理編譯時期部分;CI 中針對注入資料的資料庫執行執行時期合約與路由煙霧測試。
完整原理說明:組織章程第 XV 條,收錄於 engineering-constitution/references/engineering-constitution.md。