先寫測試最常見的辯護是紀律問題——強迫你想清楚需求、強迫測試不被跳過。這些說法都對,但它們解釋不了為什麼順序重要。
真正的理由只有一個,而且很具體:你會親眼看到那個測試失敗一次。
一、Red 這一步驗的是測試本身
一個從來沒有紅過的測試,有可能永遠是綠的。它照樣會被執行、照樣進覆蓋率報告、照樣讓人安心。
會發生這種事的原因很平凡:斷言寫得太寬鬆(expect(result).toBeDefined())、測到了錯的東西、或者 mock 把被測邏輯整段換掉了。這些錯誤不會有任何徵兆——測試通過就是它唯一會給你的訊號,而它給錯了。
先寫測試、確認它因為「功能還不存在」而失敗,就順手排除了這一整類問題。這是三個步驟裡最容易被跳過、也最不能跳過的一步:
Red 寫測試,執行,確認它失敗——而且失敗的原因是你預期的那個
Green 寫剛好能讓它通過的實作,不提前優化
Refactor 整理程式碼,測試必須保持綠燈
先寫實作再補測試也不是不行,但補完之後要記得故意把實作改壞一次,確認測試真的會紅。少了這個動作,你補的是覆蓋率,不是保護。
二、測試環境是速度與真實度的取捨
Vitest 在 Nuxt 專案裡有兩種跑法,差別不在寫法,在啟動成本:
| 適用 | 代價 | |
|---|---|---|
| Node 環境 | 純函式、格式化、計算、驗證 schema | 拿不到任何 Nuxt 的東西 |
| Nuxt 環境 | 需要 composable、auto-import、元件掛載 | 每個檔案都要付一次環境啟動 |
判準是:這段邏輯需不需要 Nuxt 才能執行。 不需要就放 Node 環境——這類測試通常在毫秒級完成,你才會願意在每次存檔時跑它們。
實務上這會反過來影響設計:把商業邏輯從元件裡抽出來變成純函式,不只是為了好測,而是為了讓它測起來夠快。測得慢的測試最後都會被關掉。
三、mock 到什麼程度才不會測到自己
mock 的目的是隔離外部依賴——網路、資料庫、時間。但每 mock 掉一層,測試涵蓋的真實範圍就少一層。
vi.mocked($fetch).mockResolvedValue({ data: mockUsers })
這行讓你能測「拿到資料之後怎麼處理」,但同時也表示「請求有沒有正確組出來」不在這個測試的守備範圍。這是合理的分工,前提是你知道那一半沒被測到。
界線大致是:mock 邊界,不 mock 邏輯。 資料庫 client、HTTP 呼叫、當前時間可以 mock;被測函式自己呼叫的另一個自家函式不要 mock——mock 了之後測試通過只證明「我的 mock 回傳了我預期的值」。
有兩種測試不必寫:測框架本身(Vue 會不會渲染元件不是你的責任)、測私有函式(它們會透過公開介面被涵蓋,直接測會讓重構寸步難行)。
那覆蓋率要到多少?這個數字本身沒有意義——它量的是「有多少行被執行過」,不是「有多少行被驗證過」。一個把所有函式都呼叫一遍、但斷言全部寫成 toBeDefined() 的測試套件可以輕鬆做到九成覆蓋率,而它什麼都保護不了。比較有用的問法是:**把某一段核心邏輯故意改壞,有沒有測試會紅?**紅了才叫有覆蓋。
四、把檢查串成一條,順序有意義
{
"scripts": {
"check": "pnpm format && pnpm lint && pnpm typecheck && pnpm test"
}
}
順序不是隨便排的:格式化會改檔案,所以要在檢查之前;型別錯誤會讓大量測試以難懂的方式失敗,所以型別要先過。前面任一步失敗就停下來,後面的輸出對排錯沒有幫助,只會把真正的錯誤訊息推到捲軸外面。
最後一條規則沒有例外:it.skip 是技術債,不是暫時措施。 被跳過的測試不會有人回來開啟它,而它在報告裡看起來跟通過幾乎一樣。真的不需要就刪掉,需要就修好——留著一個永遠不跑的測試,比沒有那個測試更糟,因為它會讓人以為那塊有保護。