Skip to main content
AI 時代的自動化測試與驗收(二):Unit Test(上)在保護什麼?

Architecture Journal

TestingUnit TestAI Engineering

AI 時代的自動化測試與驗收(二):Unit Test(上)在保護什麼?

系列定位:延續第一篇對測試金字塔與信任系統的重定義,這篇聚焦 unit test 的定義、範圍、測試對象、mock 的取捨,以及 AI 時代最常見的測試陷阱。

這篇要回答什麼

在 AI 可以快速補測試之後,unit test 最容易被誤用成兩件事:把 coverage 拉高,讓 CI 保持綠燈。

系列第一篇講過,AI 時代真正稀缺的已經不是「生不生成得出來」,而是「哪個不變量值得保護」。這件事在 unit test 這層最明顯。agent 幾秒鐘就能補一百個測試,生成測試不再是難題;難的是判斷這些測試到底有沒有保護到重要規則。

因此,本文聚焦四個問題:

  • unit test 到底是什麼?範圍到哪?這題比你想的還模糊。
  • unit test 在保護什麼?
  • 什麼該測、什麼不該 mock?
  • 它有什麼缺點,什麼時候不值得寫?

先搞清楚:「unit test」到底是什麼

先講一件比較反直覺的事:unit test 這個詞,定義一直很模糊。

Martin Fowler 在 UnitTest 這篇 bliki 裡講得很坦白,連測試培訓課程都能列出一堆不同定義。但他整理出幾個「共通元素」:低層級、聚焦在系統的一小塊;開發者自己寫,用平常的工具加一個測試框架就是全部;要夠快,快到願意頻繁執行。[1]

所以在團隊裡談 unit test 之前,通常要先對齊「unit 到底多大」。Fowler 把兩種主流叫法講得很清楚:

  • Solitary(孤獨式):測的單元盡量隔離,collaborator 全部用替身。壞在 customer class 的 bug,不會波及 order 的測試,失敗訊號很乾淨。
  • Sociable(社交式):直接用真實的 collaborator,一次測一叢行為。壞了再往下追,但一個 bug 可能引起一串測試失敗。

這不是誰對誰錯,是取捨。solitary 訊號乾淨、好定位,但很容易鎖住實作細節;sociable 比較像真實行為、比較少用替身,但需要更大的 fixture。Fowler 自己是 classic 派,偏好 sociable。[1]

另外 Vladimir Khorikov 在《Unit Testing Principles, Practices, and Patterns》裡給了一個更適合本文的現代定義:unit 不是「一個方法」,而是「單一行為單元」(unit of behavior)。測試單位可以是一個方法、一組協作物件、甚至一個服務,重點是它代表「一個可以被獨立驗證的行為」。[2] 他同時給了 unit test 三個性質:自動執行、可重複、夠快。[2]

Roy Osherove 的經典定義則特別強調 isolation:測的是「被隔離出來的一個 unit」,也因此 mock/stub 的用法會直接影響這條界線在哪。[7]

這裡特別要釐清「unit 等於方法」這個誤解,因為它正是 AI 生成一堆「測方法、不測行為」的測試的根源。把 unit 想成行為,之後的每一段才講得通。

方法會換,規則要留下

unit test 真正值得保護的,不是某個方法現在能不能執行,而是業務規則能不能在重構之後仍然成立。方法會換,規則要留下。

金融或交易系統尤其容易看出這件事。程式能跑很簡單,但金額不能為負、庫存不能超賣、退款後狀態不能再回到已出貨、利率計算的捨入規則要固定。這些規則一旦被改壞,業務就會出事,而且常常不是當下就爆,而是幾筆交易之後才看出來。

這些就是不變量(invariant)。unit test 的任務,是把不變量釘住。

判斷方式很直接:如果這條規則被改錯,哪個業務會受害?測試就該保護那個。反過來,如果一個測試刪掉也不痛、改壞了也抓不到任何業務問題,那它保護的就不是不變量,只是一個 coverage 數字。

改動變便宜之後,unit test 變成安全網

第一篇提過,agent 可以一口氣跨檔案重構。當改動變便宜,回歸風險反而上升。

這時候,unit test 的角色不是檢查清單,而是安全網。它讓開發者或 agent 敢在小步改動時往前走,不必每一步都猜「這次會不會把別的地方弄壞」。Kent Beck 在《Test-Driven Development: By Example》裡講的回饋迴路,本質上就是這件事:越小的改動配越快的測試,壞了才知道是哪一步造成的。[6]

這裡的關鍵是速度。Fowler 建議把測試套件分成兩層:compile suite,每次想編譯就跑的,要秒級;commit suite,提交前跑的,Kent Beck 的準則是十分鐘內要跑完。[1] 重點不是精確門檻,而是測試要快到團隊不會懶得跑。如果 unit test 跑一次要好幾分鐘,agent 根本不會等它,測試就會被繞過去。速度是 unit test 能不能活在 AI workflow 裡的條件,不是優化選項。

什麼該測、什麼不該 mock

一個很實用的判斷:如果 unit test 需要 mock 資料庫、HTTP、message queue,通常不是測試的問題,是設計的問題。domain 邏輯跟基礎設施纏在一起了。

六角形或 clean architecture 的價值在這裡最清楚。把規則放進不依賴框架的純邏輯,unit test 根本不需要 mock。mock 用得越多,越像在測 mock 的行為,而不是測真實的行為。

不過有些依賴確實值得替換,像是時間、亂數、外部 client 的回傳。這裡要分清楚兩種東西:

  • 測「依賴回傳 X 時,受測邏輯有沒有做對的決策」:給一個固定的假答案,這是對的。替換的是外部世界。
  • 測「某個 method 被呼叫幾次、照什麼順序」:這是在鎖定實作細節,重構一下就全碎。

要講清楚這條線,Meszaros 的 xUnit Test Patterns 是經典。他用「Test Double」當總稱,再細分成五種:Dummy、Fake、Stub、Spy、Mock。[3] [5] 重點在最後一組:Stub 給固定答案,通常配 state verification,測「結果對不對」;Mock 則預先設定期望的呼叫,配 behavior verification,測「有沒有照預先假設的方式呼叫」。[4]

Fowler 在 Mocks Aren’t Stubs 裡把這條線再往上拉一層:classical 與 mockist 兩種 TDD 風格。classical 能用真物件就用真物件,只有不好用的依賴才換掉;mockist 則對每個有行為的 collaborator 都用 mock。[4] 這不是小事,它直接影響測試跟實作的耦合度:mockist 的測試比較容易鎖死呼叫方式,一改互動方式測試就碎,這對重構很傷。[4]

一個實用的預設,是先採 classical:能用 Fake 或 Stub 給固定答案、用 state verification 驗結果,就優先這樣做。Mock 留給真的難搞的邊界,像是 cache 這種看不到狀態的地方。

更直接的警訊是:如果 unit test 需要 mock 受測 class 自己的另一個 method,或需要 mock 團隊自己寫的 repository,那多半是該重構的信號,不是該補 mock 的信號。

AI 生成測試最常踩的三個坑

第一種,鏡射實作。AI 照著實作寫測試,assert 的是當時寫法的細節,不是行為。一旦換成另一種正確寫法,測試就全碎。

第二種,重述錯誤邏輯。AI 照著 buggy 的實作寫測試,測試把 bug 固定成「正確」。之後誰都不敢改,因為改了測試就紅,但其實是測試在維護錯誤。

第三種,全 happy path。補的都是「輸入正常、輸出正常」,真正的邊界、異常、非法狀態沒人測。coverage 數字很漂亮,出事的情境全在漏網。

這三種坑,對應三個審查問題,可以直接拿去問 AI 生成的測試:

  • 把實作換成另一種正確寫法,這個測試還過嗎?不過,代表它在測實作,不在測行為。
  • 如果需求被誤解,這個測試抓得到嗎?抓不到,代表它在測細節,不在測規則。
  • 這個測試唯一會紅的理由,是不是只有「有人改了它的實作」?

這其實呼應第一篇講的「測試本身也要被審查」。AI 時代 unit test 的產能從來不是問題,審查才是。

一個可行的分工:人定義不變量,AI 補案例

一個可行的 workflow 是這樣:

  1. 人先把 domain 不變量和關鍵邊界列出來。這是「哪些規則值得保護」的判斷,AI 從空白專案猜不準。
  2. AI 依著不變量補 parameterized cases、邊界值、異常與非法狀態。
  3. 人再 review 一遍,用上面三個問題過篩,把鎖實作的測試刪掉或改寫。

這樣 unit test 就是「人的判斷加上 AI 的產能」,不是讓 AI 自己決定該測什麼。

而且,把不變量寫清楚這個動作本身,就會逼團隊把規則講明白。這個效果有時比測試本身更有價值,因為很多團隊的問題不是測試少,而是規則根本沒被寫出來。

unit test 的缺點與取捨

unit test 不是越多越好,也不是零成本。

第一個缺點,測試可能鎖死實作。behavior verification 用得越細,測試越貼著實作走,重構時就要連測試一起改。Fowler 在 Mocks Aren’t Stubs 裡明說:mockist 測試與實作耦合,改動互動方式就會弄壞測試。[4] 團隊一旦養成這個習慣,AI 生成測試時問題會加倍,因為 AI 最擅長複製現有 pattern。

第二個缺點,假安全感。coverage 是無效指標,100% 也可能全在測垃圾。tautology 測試把 bug 固定成正確,之後誰都不敢改;即使 coverage 維持 100%,重要的邊界仍然可能完全沒有被驗證。

第三個缺點,維護成本。測試是資產也是負債。鎖定低價值行為的測試,重構時反而要一起改、一起刪,變成額外負擔。Khorikov 的書花了不少篇幅在講「哪些測試該刪」,這件事在業界常常被忽略。[2]

第四個缺點,它救不了粗粒度的錯誤。unit test 只驗單一規則,元件接在一起、跨系統契約、使用者旅程都不是它的責任。把信心全押在 unit test,等於假設 bug 只會發生在單一方法裡,這在整合系統幾乎不成立。

所以務實的取捨是:對純計算、domain 規則、狀態轉換這些東西,值得寫重一點,parameterized test 用力套。但對 CRUD、簡單的傳遞層,unit test 的價值很低,硬補是過度設計。那層的風險在整合跟契約,不在單一規則。金字塔的意義,是每一層保護適合自己的風險,unit test 只負責把最底層釘住,讓上面幾層不用替它收拾殘局。

結論

unit test 的價值,在於把真正不能改壞的規則留下來。AI 可以快速補齊案例,卻不能替團隊決定哪些規則值得保護;這個判斷仍然要回到 domain、不變量和實際風險。

因此,好的 unit test 不一定很多,也不一定追求最高 coverage。它應該讓重要規則容易驗證,讓重構有安全網,也讓團隊知道哪些行為一旦改變,就必須重新討論。寫測試之前,先問清楚「我們要保護什麼」,通常比先問「這個方法要怎麼測」更重要。

參考資料