Skip to content

2026-09-13

/ 12 分鐘閱讀

/ Testing Fundamentals

測試的解剖學:npm test 從 package.json 到 vitest,以及為什麼「沒失敗」不等於「沒問題」 ​

npm test 從哪裡來 ​

講師 Steve Kinney 用課程範例應用裡的一個 scratchpad 範例,示範最開頭出現過的那個最簡單測試怎麼實際跑起來:

javascript
import { it, expect } from 'vitest';

it('is a super simple test', () => {
    expect(true).toBe(true);
});

光在編輯器裡看到測試程式碼還不夠,得實際執行它,執行的方式是在終端機下 npm test。這個指令背後其實只是 package.json 裡 scripts 欄位定義的一段命令:

json
{
    "scripts": {
        "test": "vitest"
    }
}

npm test 實際上就是去執行 vitest 這個指令。如果是全新建立的 package.json(例如跑過 npm init),預設的 test script 通常只是印出「沒有設定測試」的訊息,代表這個專案本身沒有安裝任何測試執行器。要真正跑測試,得先安裝一套測試工具(不論是 vitest 或其他框架搭配的工具),再把 test script 改成執行那套工具的指令。

測試執行器啟動後不會執行完就結束,而是會持續監看檔案變動,程式碼一有改動就自動重新執行測試。多數現代的測試執行器在這裡做了效率上的優化:只會重跑跟你剛剛改動的檔案有關聯的測試,因為沒有變動、也跟改動內容無關的測試就沒有重新執行的必要,執行時間都是成本,回饋速度越快越好。

測試失敗長什麼樣子 ​

當斷言(expectation)不成立時,測試就會失敗。畫面上會出現明顯的視覺提示,測試從綠色的通過狀態變成紅色的失敗狀態,讓開發者一眼就能看出程式碼的行為不符合預期。

測試通過不代表程式碼真的沒問題 ​

Steve 特別強調一個容易被忽略的重點:測試通過,不代表程式碼是對的,只代表這次執行沒有失敗而已。這也是為什麼即使測試覆蓋率做到 100%,仍然可能同時存在寫得很糟的測試,這點延續了前一篇提到「覆蓋率數字本身不是目標」的邏輯,一個空有覆蓋率、卻沒有真正驗證邏輯的測試,一樣沒辦法消除改動程式碼時的焦慮感。

範例一:把斷言註解掉,測試依然通過 ​

如果把測試裡唯一的斷言整行註解掉,測試依然會通過:

javascript
it('is a super simple test', () => {
    // expect(true).toBe(true);
});

測試之所以通過,只是因為它沒有失敗,而不是因為驗證了任何事情。這個函式裡沒有任何斷言可以拋出錯誤,自然也就不會有測試失敗的機會,卻也完全沒有驗證任何邏輯是否正確。

範例二:主動拋出錯誤,測試才會失敗 ​

要理解測試失敗的機制,可以看 expect 到底做了什麼:它拿到實際值,跟預期值比較,如果不相符就拋出一個錯誤(exception)。真正讓測試失敗的,是這個拋出錯誤的動作本身:

javascript
it('is a super simple test', () => {
    expect(true).toBe(true);
    throw new Error();
});

即使前面的斷言明明是成立的,只要函式裡有任何地方拋出錯誤,測試依然會失敗。單元測試在最基本的層次上,做的事就是呼叫一個函式、檢查結果、如果結果不如預期就拋出錯誤,沒有錯誤就算通過,僅此而已。

範例三:非同步程式碼裡的陷阱 ​

比起明顯的疏漏(像忘了寫斷言),更容易讓人不小心的情況出現在非同步程式碼裡。如果把斷言包在 setTimeout 裡,測試函式本身在斷言真正執行之前就已經結束了:

javascript
it('is a super simple test', () => {
    setTimeout(() => {
        expect(true).toBe(false);
    }, 1000);
});

這裡的斷言明明是錯的(true 不等於 false),但測試依然會通過,因為測試在 setTimeout 裡的回呼函式真正執行之前就已經跑完了,根本沒有機會拋出錯誤。這正是延續前一篇提到「已知情境測試」跟「沒預料到的邊界案例」之間的落差之一:非同步程式碼很容易讓測試在還沒真正驗證到任何東西之前,就已經提前判定通過。課程後段會回頭處理如何正確等待非同步結果再做斷言,這裡先點出問題本身。

測試的核心機制,其實就這麼簡單 ​

拆到最底層,測試做的事情永遠是同一件事:呼叫一個函式,檢查結果,如果任何地方拋出錯誤,整個測試套件就會變成紅色,代表有東西沒有通過。沒有錯誤發生,測試就通過。這就是測試機制的全部。

複習 ​

測試通過實際上代表程式碼運作正常嗎? ​

測試通過不代表程式碼真的沒問題,只代表這次執行沒有失敗。即使實作有缺陷或不完整,測試依然可能通過

究竟是什麼原因讓測試失敗? ​

測試失敗是因為拋出了例外,通常是實際結果跟預期結果不一致,或程式碼中主動拋出了一個錯誤

測試非同步程式碼有什麼潛在問題? ​

如果測試在非同步操作完成之前就先執行完畢,測試可能會提前通過,因而漏掉非同步程式碼裡真正的錯誤或不正確的行為

單元測試的基本結構是什麼? ​

單元測試呼叫一個函式、檢查結果,如果結果不如預期就拋出錯誤;沒有遇到錯誤,測試就算通過

為什麼即使程式碼被註解掉或寫錯,測試還是可能通過? ​

因為測試通過的條件只是「沒有失敗」,並不保證真的驗證了程式碼的正確行為,單純沒有拋出錯誤並不等於實作是正確的

小測驗 ​

根據測試的基本原理,非同步程式碼為什麼會讓測試在斷言失敗的情況下仍然通過? 因為測試在斷言真正執行之前就已經先執行完畢
多數現代測試執行器如何優化測試的執行效率? 只重新執行跟異動檔案相關的測試
是什麼原因導致測試套件裡的一個測試失敗? 執行過程中拋出了錯誤或例外

此文章是 FrontendMasters 上的 Testing Fundamentals 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 3 篇 / 共 27 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash