測試覆蓋率是那種每個人都同意重要、但沒人想負責提升的指標。你一定見過這樣的模式:一個程式碼庫從 90% 的覆蓋率開始,然後幾個功能在沒有測試的情況下上線,接著一次重構跳過了測試更新,因為「我們之後會回來補」。六個月後覆蓋率剩 60%,要爬回去感覺像一份沒人報名的全職工作。
這篇案例研究詳細說明一個團隊如何使用 JieGou 工作流程搭配 Coding Agent,在三週內將測試覆蓋率從 60% 提升到 94% — 全自動執行,幾乎不需要人工介入。
問題
這個程式碼庫是一個包含 312 個模組的 Node.js 後端。覆蓋率已經持續下降了好幾個月:
- 整體測試覆蓋率 60%,從年初的 85% 一路下滑
- 47 個模組完全沒有測試 — 主要是工具函式、資料轉換器和驗證邏輯
- 新功能在沒有測試的情況下上線,因為開發人員專注於交付期限
- CI 管線會執行測試但不強制覆蓋率門檻,所以下降趨勢在有人檢查之前都是隱形的
團隊曾兩次嘗試安排「測試撰寫衝刺」。兩次都因為客戶緊急事件而被降低優先順序。手動撰寫測試既緩慢又枯燥,而且與其他所有優先事項競爭。
工作流程
團隊在 JieGou 中建構了一個 4 步驟的工作流程,在程式碼合併到 main 時自動觸發。除非產生的測試需要修改,否則不需要任何人工介入。
步驟 1:觸發 — PR 合併時的 GitHub webhook
工作流程從一個 webhook 觸發器開始,當 pull request 合併到 main 時觸發。
trigger:
type: webhook
source: github
events: ["pull_request.closed"]
filters:
- field: "action"
value: "closed"
- field: "pull_request.merged"
value: true
- field: "pull_request.base.ref"
value: "main"
Webhook 載荷包含變更檔案清單、合併 commit SHA 和 PR 中繼資料。這些資訊全部傳入下一個步驟。
步驟 2:分析 — 識別未測試的函式
一個 Recipe 步驟從 webhook 取得變更檔案清單,並與最新的覆蓋率報告交叉比對。Recipe 的 prompt 很直接:
step: analyze-coverage
type: recipe
model: claude-sonnet-4-6
input:
changed_files: "{{trigger.pull_request.changed_files}}"
coverage_report: "{{secrets.COVERAGE_REPORT_URL}}"
prompt: |
你是一位測試覆蓋率分析師。根據變更檔案清單和覆蓋率報告
(lcov 格式),識別:
1. 哪些變更檔案的行覆蓋率低於 80%
2. 這些檔案中哪些匯出的函式覆蓋率為零
3. 函式簽名和每個函式的簡要描述
輸出一個 JSON 陣列,物件欄位為:
file_path, function_name, signature, description, current_coverage
這個步驟產生一份精確的清單,列出哪些函式需要測試。它會過濾掉已經有足夠覆蓋率的檔案,這樣 Coding Agent 就不會在已測試的程式碼上浪費時間。
步驟 3:產生 — Coding Agent 撰寫測試
這是 Coding Agent 發揮主要作用的地方。它接收未測試函式的清單、複製儲存庫,然後為每個函式撰寫單元測試。
step: generate-tests
type: coding-agent
model: claude-sonnet-4-6
repo: "{{secrets.REPO_URL}}"
branch: "test-gen/{{trigger.pull_request.number}}"
maxTurns: 30
sandbox:
memory: "1GB"
timeout: "5m"
network: false
tools:
- read
- write
- edit
- bash
- glob
- grep
input:
untested_functions: "{{steps.analyze-coverage.output}}"
task: |
你是一位資深測試工程師。你的任務是為以下未測試的函式撰寫
單元測試:
{{untested_functions}}
指示:
1. 讀取每個函式的原始檔案以了解其行為
2. 讀取同目錄中的現有測試檔案以了解風格慣例
3. 使用專案的測試框架(vitest)撰寫測試
4. 每個測試檔案應涵蓋:正常路徑、邊界情況、錯誤處理
5. 撰寫每個測試檔案後執行
`npm test -- --reporter=verbose <test-file>` 以驗證所有
測試通過
6. 如果測試失敗,讀取錯誤訊息、修復測試並重新執行
7. 不要修改原始程式碼 — 只建立或更新測試檔案
匹配現有的測試風格:describe 區塊、清晰的測試名稱、
arrange-act-assert 模式。使用現有的 fixtures 和 helpers。
關鍵的設定選擇:
- 模型:Claude Sonnet 4.6 — 速度足以應付大量測試產生,智慧足以理解複雜的函式簽名和邊界情況。
- 沙箱:1 GB 記憶體、5 分鐘逾時 — 為
npm test執行提供足夠的空間,又不會讓失控的程序佔用資源。 - 停用網路 — 代理不需要外部存取;所有內容都在複製的儲存庫中。
- maxTurns: 30 — 允許代理在多個測試檔案之間迭代,並在多個循環中修復失敗。
任務描述中包含一條關鍵指示:先讀取現有的測試檔案。這確保產生的測試匹配團隊的慣例 — 相同的斷言風格、相同的 describe 區塊結構、相同的 fixture 模式。沒有這條指示,產生的測試在功能上是正確的,但風格不一致,這會在審查時產生摩擦。
步驟 4:提交 — 建立包含結果的 PR
Coding Agent 完成後,最後一個 Recipe 步驟會建立一個包含產生測試的 pull request,並在 PR 描述中附上測試結果。
step: submit-pr
type: recipe
model: claude-sonnet-4-6
input:
modified_files: "{{steps.generate-tests.modified_files}}"
test_output: "{{steps.generate-tests.output}}"
source_pr: "{{trigger.pull_request.number}}"
prompt: |
建立一個 pull request,包含以下內容:
- 標題:"test: auto-generated tests for PR #{{source_pr}}"
- 內容:包含新增測試的摘要、涵蓋的函式,
以及顯示所有測試通過的完整測試輸出
- 標籤:"auto-tests"
- 要求原始 PR 作者進行審查
PR 標記為 auto-tests,讓團隊可以篩選並批次審查產生的測試 PR。要求原始 PR 作者進行審查,意味著最熟悉程式碼的人會最先看到這些測試。
結果
團隊在三週內對每次合併到 main 的操作執行了這個工作流程。以下是數據:
| 指標 | 數值 |
|---|---|
| 起始覆蓋率 | 60% |
| 最終覆蓋率 | 94% |
| 產生的測試數量 | 847 |
| 建立的 Pull Request 數量 | 42 |
| 平均 PR 審查時間 | 12 分鐘 |
| 發現現有 bug 的測試數量 | 23 |
| 每日平均成本 | 約 $4.50 美元(Claude API) |
有幾個值得注意的重點。
42 個 PR 中產生了 847 個測試,意味著每個 PR 大約 20 個測試。大多數 PR 涵蓋 2-4 個原始檔案,每個函式有 5-6 個測試(正常路徑、邊界條件、錯誤情況、null/undefined 輸入、型別強制轉換邊界情況)。
平均 12 分鐘的審查時間非常出色。大多數產生的測試在第一次提交時就是正確的。主要的審查意見是風格相關的 — 重新命名測試描述、調整 fixture 資料使其更貼近實際。很少有測試需要功能性的修正。
23 個測試發現了現有程式碼中的實際 bug。這是最有趣的結果。Coding Agent 為一個日期解析函式撰寫了測試,該函式預期 ISO 8601 格式,而測試揭露了該函式對帶有時區偏移的時間戳會靜默回傳 Invalid Date。這個 bug 已經在生產環境中存在了四個月。類似的發現還包括驗證邏輯中的問題、分頁 helper 中的差一錯誤,以及快取模組中的競態條件。
每日 $4.50 美元的 Claude API 成本與節省的工程時間相比微不足道。保守估計:以每個測試 15 分鐘的速度手動撰寫 847 個測試,大約需要 212 小時 — 超過一位全職工程師五週的工作量。
經驗教訓
在執行這個工作流程三週後,團隊根據哪些方法有效、哪些無效,改進了他們的做法。
從純函式開始
第一個版本的工作流程針對所有未測試的程式碼。這對純函式效果很好 — 工具函式、驗證器、轉換器、格式化器 — 因為它們有明確的輸入、明確的輸出,而且沒有副作用。代理可以讀取函式、理解契約,然後撰寫全面的測試。
具有資料庫連線、外部 API 呼叫和共享可變狀態的複雜模組則較為困難。代理有時會產生過度 mock 或測試實作細節而非行為的測試。團隊調整了分析步驟,優先處理純函式,並將複雜模組排入人工審查佇列。
提供現有測試範例
影響最大的單一改變是在任務描述中加入這條指示:「讀取同目錄中的現有測試檔案以了解風格慣例。」在加入這條指示之前,代理產生的測試在功能上是正確的,但使用了不同的斷言模式、不同的 describe 區塊結構和不同的命名慣例。加入指示之後,產生的測試與手寫的幾乎無法區分。
在建立 PR 之前執行測試
早期版本的工作流程提交的 PR 中包含未經驗證的測試。有些有 import 錯誤、缺少 fixtures 或斷言失敗。在 Coding Agent 任務中加入 npm test 步驟 — 並指示它在完成前修復失敗 — 幾乎消除了所有這些問題。代理的迭代循環(撰寫測試、執行測試、讀取錯誤、修復測試、重新執行)與人類的工作方式完全相同,只是更快。
審查品質,而非僅僅覆蓋率
覆蓋率是一個代理指標。一個斷言 expect(result).toBeDefined() 的測試在技術上覆蓋了函式,但不會驗證行為。團隊為產生的測試 PR 新增了一份審查清單:
- 每個測試是否驗證了有意義的行為,而不僅僅是函式能執行?
- 是否涵蓋了邊界情況(null 輸入、空陣列、邊界值)?
- 測試描述是否清楚說明正在測試的行為?
- 斷言是否具體(
.toBe(42)而非.toBeTruthy())?
大多數產生的測試無需修改即可通過這份清單。少數未通過的在 12 分鐘的審查中也很容易發現和修正。
完整設定
對於想要複製此工作流程的團隊,以下是完整的步驟設定:
workflow:
name: "Auto Test Generation"
description: "在 PR 合併時為未測試的程式碼產生單元測試"
trigger:
type: webhook
source: github
events: ["pull_request.closed"]
filters:
- field: "pull_request.merged"
value: true
- field: "pull_request.base.ref"
value: "main"
steps:
- id: analyze-coverage
type: recipe
model: claude-sonnet-4-6
# ...(參見上方步驟 2)
- id: generate-tests
type: coding-agent
model: claude-sonnet-4-6
dependsOn: [analyze-coverage]
# ...(參見上方步驟 3)
- id: submit-pr
type: recipe
model: claude-sonnet-4-6
dependsOn: [generate-tests]
# ...(參見上方步驟 4)
工作流程端到端執行時間為 8-15 分鐘,取決於未測試函式的數量。大部分時間花在 Coding Agent 迭代處理測試檔案以及在每個檔案撰寫完成後執行 npm test。
下一步
團隊目前正在兩個方向擴展這個工作流程:
- 整合測試 — 一個獨立的 Coding Agent 步驟,使用現有的測試 helper 和資料庫 fixtures 為 API 端點產生整合測試
- 覆蓋率強制 — 一個 CI 檢查,當覆蓋率低於 90% 時讓建構失敗,因為基線已經夠高可以強制執行了
更廣泛的啟示:測試覆蓋率不必是一項手動苦差事。使用正確的工作流程,你可以將它視為與現有 CI/CD 並行運作的自動化管線 — 不需要專門的衝刺、不會被降低優先順序、沒有覆蓋率技術債。
Coding Agent 在 Pro 方案及以上可用。建構你的第一個編碼工作流程。