工程 · 2026.10.03 · 4 min · 更新於 2026.10.26
覆蓋率很高還是漏測?用 CRAP 指標找出最該補測試的函式
程式多半是 agent 寫的,行覆蓋率很高,卻看不出沒測到的地方是不是落在最複雜的函式。導入 CRAP 指標、門檻定在 8 之後,前後端清掉兩百多個超標函式,也修了 72 個讀程式時發現的問題。
閱讀設定
測試的行覆蓋率很高,還是看不出剩下沒測到的幾行,是不是剛好落在最複雜的函式裡。
我這個 Rust 與 React 專案的程式多半是 agent 寫的,整體行覆蓋率很高。導入 CRAP 指標之後,發現確實有這種情況:後端用寬鬆的門檻就抓到排程器這類有分支沒測到的函式,前端的請求管線完全沒有測試。
最後門檻定在 8,前後端都清到 0 個超標,另外修了 72 個讀程式時發現的問題。
CRAP 在算什麼
CRAP(Change Risk Anti-Patterns)替每個函式算一個分數:
CRAP = 複雜度² × (1 − 覆蓋率)³ + 複雜度
複雜度指的是循環複雜度,算函式裡有幾條獨立的執行路徑,分支越多越高。
分支多又沒測到,分數就高;測滿的函式,分數等於它的複雜度。
我要的是一份指出「哪個函式該補測試或該拆」的清單,加上 PR 階段的檢查。後端用現成的 cargo-crap 加 cargo-llvm-cov,前端隔天跟上。
門檻 8,只查改到的檔案
門檻先用工具預設的 30,整個後端 2,670 個函式只抓到 3 個:排程器、Discord 維運通知、duration 解析。
三個都是真的有分支沒測到,只是 30 對這個 codebase 太鬆。
這三個函式和前端沒測過的請求管線,都是在行覆蓋率很高時沒被注意到的。CRAP 把它們排到清單最上面,先補哪裡就有了依據。
定 6 的話,三個 if 加一個 match 的函式測滿都過不了,最後定 8。測滿的函式分數等於複雜度,所以 8 同時也是複雜度的上限。

Rust 的 ? 運算子遇到錯誤會直接往上回傳。cargo-crap 預設每個 ? 加 1 分,但錯誤往上傳的那條路徑,行覆蓋率量不到。
算進去的話,一長串 DB 呼叫的函式測滿了分數還是高,結帳函式的複雜度會從 7 變成 21。所以我設成 ? 不算分支。
PR 規則是改到的檔案裡不能有超標函式,沒改到的不看。
這樣不需要 baseline(既有違規的基準清單),既有的超標函式也不會擋住不相干的 PR。
全 repo 任一函式超標就失敗的做法沒採用,因為 main 上只要還有一個,所有 PR 都會紅。
後端的檢查另開一個 job,沒動後端的 PR 不觸發。它沒設成 GitHub 的 required check,因為那樣每個 PR 都要回報、多算一分鐘額度。
改由 merge 用的 script 擋,細節在用 merge script 與部署預檢守住 main。
前端用 TypeScript AST 計分
前端在這之前只有 8 個 Playwright 測試,沒有單元測試。
我定的規則是 ?. 和 ?? 不算分支。現成的 crap4ts 和 ESLint 的 complexity 規則都沒有設定可以關,所以計分腳本自己用 TypeScript 的 AST(抽象語法樹,編譯器解析原始碼後的樹狀結構)寫。
覆蓋率要連分支出口一起算:if 和三元的兩邊、&& 和 || 的每個運算元都走過,才算測滿。
前端的分支多半寫在同一個敘述裡,像 JSX 的 {cond && …}。只算行的話,元件渲染一次就 100%,分數沒有意義。
覆蓋率工具用 Vitest 的 istanbul,因為 v8 實測會把提早 return 之後沒執行到的行,算成有執行。
清理兩百多個超標函式
門檻改成 8 的當下,後端有 120 個函式超標,前端有 88 個。
過渡用一份待清清單:清單內的檔案暫時沿用舊門檻,其他檔案立刻適用 8,清完一個移除一個。中途停在哪一步,已經清好的都不會退回去。

原則是沒人用的刪掉、沒測到的補測試,真的有分支邏輯的才拆,而且是原地搬進有名字的函式,不改行為。
前端頁面拆之前,先對原本的頁面把測試寫到全過。拆完比對每一個 DOM 狀態,以及 API、導頁的呼叫順序,共 966 個情境。
這層對照真的抓到拆錯的地方:把一段 await 抽成新的 async 函式之後,API 呼叫和導頁的先後順序互換了。
補測試時讀到的問題,另外開 issue 修掉,例如:
- 新登入通知把 iPhone、iPad 標成 macOS,因為判斷作業系統時先比到
Mac OS X字串 - 註冊查重用沒轉小寫的 email,大小寫不同的重複註冊會回 500
- 暫停 Bot 時讓使用者選的原因只存在畫面上,根本沒送出去
代價:測試數與 CI 時間
導入前後的測試數和前端 CI 時間:
| 項目 | 導入前 | 導入後 |
|---|---|---|
| 後端測試 | 2,655 個 | 2,767 個 |
| 前端測試 | 0 個 | 2,604 個 |
| 前端 CI 檢查 | 不到 1 分鐘 | 1 分 47 秒 |
每個前端 PR 因此多算一分鐘額度。

工程
抓 Rust 與 TypeScript 的循環依賴:自己寫模組依賴掃描器,分層規則放進 CI
後端 18 個 package 之間有好幾組循環依賴,只靠人記。我寫了一支不用編譯的掃描器,產出能點進程式碼的架構圖、把分層規則放進 CI,同一天清掉 23 條既有違規,只有 2 條需要注入。
留言
載入留言中…