想像一下這個場景:你修改了一行程式碼,按下 commit,然後……等了整整一個小時,CI 才跑完所有測試告訴你「通過」。對於只有幾百個測試的小專案,這或許還能接受;但如果你是 Meta 的工程師,面對的是一個擁有數百萬個測試的 monorepo,每次都跑全部測試根本不可能——不論是時間、運算資源、還是工程師的耐心都撐不住。
那 Meta 是怎麼做到讓每個工程師每天都能快速提交幾十個 diff,而且每個都能得到可靠的測試回饋?答案之一就是一套叫做 JIT Tests(Just-In-Time Tests) 的機制。
這篇文章會帶你完整理解 JIT Tests 的運作原理,並透過一個具體的範例來說明它如何在實際專案中發揮作用。
什麼是 JIT Tests?
JIT(Just-In-Time) 直譯是「即時」,這個詞最常出現在編譯器領域(例如 Java 的 JIT 編譯器),意思是「需要時才做」。把這個概念套用到測試上,JIT Tests 就是:
當程式碼變更發生時,系統即時挑選並執行與這次變更真正相關的測試,而不是盲目跑整個測試庫。
核心理念可以用一句話總結:不要浪費時間跑無關的測試。
這聽起來簡單,但要在 Meta 這種規模的 codebase 上實作,背後牽涉到相依性分析、機器學習、分散式運算、歷史資料探勘等多種技術。
為什麼需要 JIT Tests?
要理解 JIT Tests 的價值,先看看「沒有它」會發生什麼事。
假設一個 monorepo 裡有 100,000 個測試,每個測試平均跑 2 秒。如果每個 diff 都要跑全部測試:
- 序列執行需要 200,000 秒(約 55 小時)
- 就算用 1,000 台機器平行跑,也要 200 秒(超過 3 分鐘)
- 如果一天有 10,000 個 diff,總運算時間是 2 億秒 = 約 6 年的 CPU 時間
這還是理想情況。真實情況下,很多測試會因為環境問題、網路抖動、競態條件而變得不穩定(flaky),進一步拖慢回饋速度。
但事實上,絕大多數測試跟這次變更根本沒關係。如果你只改了一個定價模組,為什麼要跑使用者登入的測試?JIT Tests 就是要解決這個浪費。
JIT Tests 的運作流程
整個機制可以拆成五個階段,我們一步步來看。
階段一:工程師提交 Diff
工程師寫完程式碼後,透過版本控制工具提交變更(在 Meta 內部叫做 diff,概念上類似 GitHub 的 Pull Request)。這個 diff 包含了「哪些檔案被改了、改了什麼」的完整資訊。
階段二:依賴分析(Dependency Analysis)
系統接手後第一件事,就是分析這次變更「碰到了什麼」。它會透過 build graph(建置相依圖) 往外推算出影響範圍:
- 直接影響:哪些測試直接測試了被修改的程式碼?
- 間接影響:哪些測試透過函式呼叫鏈、模組引用,間接依賴這段程式碼?
這張相依圖是整套機制的骨幹。沒有準確的相依圖,就沒有精準的測試挑選。
階段三:測試挑選(Test Selection)
依賴分析只是第一關。真正聰明的 JIT 系統會再加上一層「智慧挑選」,考慮:
- 歷史失敗率:這個測試過去是不是常因為類似變更而失敗?
- 穩定性:這個測試是否被標記為 flaky(偶爾會無故失敗)?
- 執行成本:跑這個測試要花多少時間和資源?
- 機器學習預測:基於過去數千萬筆歷史資料,模型預測這個測試在這次變更下失敗的機率是多少?
最後一點特別值得一提——Meta 曾經公開發表過一種技術叫 Predictive Test Selection(預測性測試挑選),它訓練一個機器學習模型來預測「某個測試在某個變更下失敗的機率」。這讓系統能夠挑出那些「相依圖上看似無關、但歷史上常一起壞掉」的測試,補足純靜態分析的盲點。
階段四:平行執行(Parallel Execution)
挑選完的測試集合會被送到分散式測試基礎設施上平行執行。Meta 內部有專門的系統(例如 TestX、Sandcastle)來調度這些任務,確保測試能在最短時間內跑完。
階段五:結果回報
測試結果會即時回饋到 diff 的 code review 介面。工程師不用離開工作環境,就能看到哪些測試通過、哪些失敗、失敗的原因是什麼。只有全部通過(或明確忽略的失敗),diff 才能合併進主幹。
一個完整的範例
光看流程還是抽象,我們用一個具體的例子把整套機制走過一遍。
專案結構
假設 Meta 內部有一個電商系統的 monorepo,結構簡化如下:
/shop ├── cart.py # 購物車邏輯 ├── pricing.py # 定價與折扣計算 ├── checkout.py # 結帳流程(會呼叫 cart 和 pricing) ├── user.py # 使用者資料 └── tests/ ├── test_cart.py # 測 cart.py ├── test_pricing.py # 測 pricing.py ├── test_checkout.py # 測 checkout.py(間接依賴 cart 和 pricing) └── test_user.py # 測 user.py
這只是示意,真實的 Meta monorepo 有數百萬個檔案和測試,但運作原理完全一樣。
情境:工程師修改了 pricing.py
工程師 David 今天的任務是調整折扣計算邏輯,所以他只修改了 pricing.py,然後提交一個 diff。
比較:有無 JIT Tests 的差別
❌ 沒有 JIT Tests(傳統做法)
系統不管三七二十一,把全部 4 個測試檔案都跑一遍,包括完全無關的 test_user.py。如果 codebase 有 10 萬個測試,David 可能要等一小時才知道結果。而且這段時間,運算資源被大量浪費在不相關的測試上。
✅ 有 JIT Tests
讓我們逐步看 JIT 系統怎麼處理這個 diff。
Step 1:依賴分析建立相依圖
系統掃描 build graph,畫出這樣的關係:
pricing.py ←── test_pricing.py (直接依賴)pricing.py ←── checkout.py ←── test_checkout.py (間接依賴)
user.py 和 cart.py 的呼叫鏈都沒碰到 pricing.py,所以 test_user.py 和 test_cart.py 被排除在外。
Step 2:挑選候選測試
JIT 系統初步挑出兩個候選測試:
test_pricing.py— 直接測試被修改的檔案,必跑test_checkout.py— checkout 會呼叫 pricing,可能間接受影響
Step 3:加上歷史情報
系統進一步查詢這兩個測試的歷史資料:
test_pricing.py:過去一年跑了 5,000 次,失敗率 0.1%,平均耗時 1.8 秒 → 穩定,正常跑一次test_checkout.py:過去一年跑了 3,000 次,失敗率 2%,被標記為偶爾 flaky → 跑兩次,任一次成功就算通過
Step 4:平行執行
兩個測試同時分派到不同機器上執行,幾秒內就有結果。
Step 5:David 看到的結果
David 回到 diff 頁面,看到:
✅ test_pricing.py PASS (1.8s)✅ test_checkout.py PASS (3.2s)⏭️ test_cart.py SKIPPED (not affected)⏭️ test_user.py SKIPPED (not affected)Total time: 3.2s | Tests run: 2 | Tests skipped: 2
不到 4 秒,David 就拿到了可靠的回饋,可以安心合併。而如果跑全部測試,可能要幾分鐘甚至更久。
JIT Tests 的關鍵特徵
走完整個流程之後,可以歸納出 JIT Tests 幾個核心特徵。
精準性 是核心價值。透過 build graph 準確掌握相依關係,做到「不多跑一個、不漏跑一個」。漏跑會讓壞掉的程式碼混進主幹;多跑會浪費資源、拖慢回饋。兩者都要避免。
速度 是最直接的受益。從跑幾萬個測試變成跑幾十個,回饋時間從小時級縮到秒級。這對開發者體驗的提升是巨大的——工程師可以在一個上午提交十幾個 diff,而不是一個 diff 等半天。
可擴展性 讓這套機制能支撐 Meta 的規模。每天數萬個 diff、數百萬個測試、數十萬台運算節點,這套系統必須能平滑擴展而不崩潰。
Flaky 測試處理 是現實層面的補丁。任何大型 codebase 都會有不穩定的測試,JIT 系統會自動重試、隔離、或標記這些測試,避免它們誤殺無辜的 diff,同時提供資料給工程師慢慢修復。
智慧學習 是進階版。結合機器學習之後,系統能從歷史資料中學到相依圖看不出來的模式,挑選測試的精準度還會再往上一個層級。
JIT Tests 的不同稱呼
「JIT Tests」不是業界唯一的說法。類似的概念在不同公司、不同文獻中有各種叫法:
- Test Impact Analysis(TIA) — 強調「分析變更影響」的角度
- Predictive Test Selection(PTS) — 強調「用機器學習預測」的角度
- Affected Tests — 最直白的說法,「受影響的測試」
- Selective Testing — 強調「選擇性執行」的角度
這些詞大方向都指向同一件事:只跑相關的測試。差別在於實作細節和強調的面向。Google、Microsoft、Uber 等公司都有自己的類似系統。
可以從中學到什麼?
即使你不在 Meta 工作,JIT Tests 的思維對任何規模的專案都有啟發。
測試策略要跟 codebase 規模匹配。小專案跑全部測試沒問題,但當測試數量增長到一定規模,就該思考如何「只跑相關的」。不少 CI 工具(如 Bazel、Nx、Turborepo)已經內建了 affected tests 的概念,善用它們可以大幅加速你的 pipeline。
相依圖是工程生產力的基礎設施。有了準確的相依圖,不只測試可以加速,連建置、部署、程式碼審查分派都能更聰明。值得在專案早期就投資工具鏈,讓相依關係變得可查詢、可分析。
資料驅動的決策比直覺更可靠。歷史失敗率、執行時間、flaky 程度——這些數據平常沒人看,但聚合起來可以做出比人類直覺更好的決策。工程文化中「測量、分析、優化」的循環,是大規模系統能夠運作的關鍵。
小結
Meta 的 JIT Tests 機制,本質上是在資源有限的現實下,用工程方法追求「完整測試覆蓋率」與「快速開發回饋」之間的平衡。透過相依分析、歷史資料、機器學習的組合,他們讓「每次提交都跑相關測試」這件事在百萬級 codebase 上也能秒級完成。
這不是單一技術的勝利,而是系統思維的展現——把看似無解的規模問題,拆解成一個個可以優化的子問題。對任何在寫 code、做測試、搞 CI/CD 的工程師來說,這都是值得學習的範例。
如果你的團隊還在用「每次都跑全部測試」的策略,或許是時候思考:我們的 codebase 規模是不是已經到了該引入 affected tests 的時候了?
發表迴響