為什麼要測試
這個分類的所有文章皆以 TypeScript 搭配 Vitest 示範。
測試的目的是保證程式碼品質的下限,沒有測試的程式碼近乎沒有下限,通常會耗費更多時間來定位 bug,也不容易根除問題。
假設我們定義好了一些流程,例如:
- 檢查使用者輸入的 email 是否已被註冊過,才允許下一步的流程
- 日期資料沒有符合
'YYYY-MM-DD'就拋出錯誤
測試的程式碼主要用來檢驗這些流程的運行結果是否符合預期。
測試的類型
單元測試
單元測試 (Unit Test) 的規模可以是一個函式、類別、模組,主要針對小範圍的邏輯。
所以:規模小 = 容易撰寫 = 最快看到收益
單元測試專注在驗證單一功能的各類情境,因此該功能需要呼叫的外部程式碼大多會使用假資料模擬。
例如:
A 功能需要的資料是呼叫來自其他模組的 B 函式得到,這時會模擬一個假的 B 函式,並假定 B 函式會產生出指定資料。測試運行時就會呼叫模擬的 B 函式,所以 A 功能的測試只需要專注在拿到各種資料時怎麼輸出。
案例:
- 後端:repository、service 等業務邏輯的資料操作
- 前端:hooks、UI 元件的狀態變化
整合測試
整合測試 (Integration Test) 需要驗證多個模組的交互流程,通常是用來測試一些重要的業務邏輯是否正確運行,所以假資料的模擬會更少。
案例:
- 後端:API 呼叫 → service → repository → 資料庫的完整流程
- 前端:表單提交 → API 呼叫 → 狀態更新 → UI 渲染
端對端測試
端對端測試 (End to End Test) 會以使用者的角度來驗證完整操作流程。
後端要保證 API 端點的存取流程,前端要模擬使用者的 UI 操作流程。所以嚴格來說,請一個人實際從畫面上操作功能也是一種 E2E 測試(全民公測)。
案例:
- 後端:註冊 → 填寫使用者資料 → 寫入資料庫返回完整的使用者資料
- 前端:模擬使用者點擊、輸入、導航的完整操作
覆蓋率
覆蓋率 (Coverage) 是指測試的程式碼執行時涵蓋了多少原始的程式碼,常見指標包括:
- 行覆蓋率 (Line Coverage):執行過的程式碼行數比例
- 分支覆蓋率 (Branch Coverage):執行過的條件分支比例 (if / else)
- 函式覆蓋率 (function Coverage):執行過的函式比例
- 語句覆蓋率 (Statement Coverage):最常用的指標,雖然也是看行數,但與行覆蓋率不一樣的是同一行裡面有三元運算、短路等等的連續判斷,只會計算到有被測試程式碼執行過的部分
覆蓋率高 ≠ 測試品質好,重點是該功能的測試方向是否符合需求。
起手式
測試區塊
輸出兩數總和:
export function sum(a: number, b: number): number {
return a + b;
}
要測試的功能會使用 describe 包住,稱為測試區塊 (test suite):
describe('sum 函式', () => {
it('1 加 2 應該等於 3', () => {
expect(sum(1, 2)).toBe(3);
});
it('1 加 -1 應該等於 0', () => {
expect(sum(1, -1)).toBe(0);
});
});
測試區塊中會使用 it / test(功能相同) 來列舉要測試的情境,稱為測試案例 (test case)。
describe 與 it 的參數結構相同,第一個參數可以輸入文字描述,英文的描述慣例是 should...。
測試的運行結果也會將這段描述顯示出來:

3A Pattern
3A 是測試的撰寫慣例:
- Arrange(準備): 設定測試資料
- Act(執行): 呼叫要測試的函數
- Assert(驗證): 檢查結果是否符合預期
測試案例通常都按此結構撰寫!
以下的函式會輸出陣列中所有數字的總和:
export function sumArray(arr: number[]): number {
return arr.reduce((acc, curr) => acc + curr, 0);
}
依照 3A 將測試的程式碼分出區塊:
describe('sumArray 函式', () => {
it('[1, 2, 3] 應該等於 6', () => {
// Arrange
const mockInput = [1, 2, 3];
// Act
const result = sumArray(mockInput);
// Assert
expect(result).toBe(6);
});
});
測試案例需要確保每個案例都要能獨立執行,所以要模擬什麼部分、資料清理的週期都需要好好思考。如果測試案例執行之間會互相干擾測試資料與結果,整個測試就是不穩定、不準確的。
制定測試案例
測試的目的在於驗證功能行為是否符合預期,而不是在強調每個步驟都要做對,反過來說,先確定這個行為應該造成什麼結果,就會制定出正確的步驟。
所以測試案例不需要窮舉。
例如範例的 sumArray,因為只有一個參數,測試案例只需要:
- 確認這個函式會回傳一個正確加總過的數字
- 傳入空陣列會回傳什麼
- 傳入非陣列的參數會回傳什麼(這會在靜態檢查階段擋下,通常不需要測試這個案例)
小結

標準的測試金字塔會長這樣,可以直覺地對應到:
單元 = 顆粒小 = 數量多端對端 = 顆粒大 = 數量少
但這是「標準」狀況,實際開發會歷經許多階段和突發狀況,會調整各種測試類型的比例,因此有個重要守則:
不要在需求不明確的情況下開始構思測試策略。
雖然「越早導入測試越好」是一個良好習慣,但像是 PoC 這種專案輪廓還沒有成形的階段,會反覆地探索需求、測試可行性與市場價值,這個時期的需求會快速變動,可能過一晚又推翻了,這時候去拚測試的覆蓋率意義不大,還是可以寫,但是先圍繞在核心業務邏輯來測試。