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

描述不要出現不符合功能的描述,尤其是後續新增或修改功能後,測試案例多少都會有些調整,舊的描述必須要一起更新。
3A Pattern
測試的架構通常都按此結構撰寫:
- Arrange: 設定測試資料
- Act: 呼叫被測試的主體
- Assert: 檢查結果是否符合預期
以下的函式會輸出陣列中所有數字的總和:
export function sumArray(arr: number[]): number { return arr.reduce((acc, curr) => acc + curr, 0);}依照 3A 將測試的程式碼分出區塊:
describe('sumArray 函式', () => { test('[1, 2, 3] 應該等於 6', () => { // Arrange const input = [1, 2, 3]; const expected = 6;
// Act const actual = sumArray(mockInput);
// Assert expect(actual).toBe(expected); });});測試案例需要確保每個案例都能獨立執行,所以要模擬什麼部分、共用的資料清理要不要重置等等的設定,都要持續思考。如果測試案例執行時會互相干擾結果,整個測試就是不穩定、不準確的。
制定測試案例
測試的目的在於驗證功能行為是否符合預期,而不是在強調每個步驟都要做對,反過來說,先確定這個行為應該造成什麼結果,自然就會制定出正確的步驟。
所以測試案例不需要窮舉所有情境,只要優先確保正常的操作 (happy path) 與特殊的邊界條件下可以正確運行就好。
例如剛剛的 sumArray,因為只有 1 個參數,測試案例就會優先規劃:
- 確認這個函式會回傳一個正確加總過的數字
- 傳入空陣列會回傳什麼
- 傳入非陣列的參數會回傳什麼 (靜態檢查階段會擋下,通常不需要測試這個案例)
- 傳入負數或小數怎麼加總 (資料的邊界)
小結

標準的測試金字塔會長這樣,對應到:
單元 = 顆粒小 = 數量多端對端 = 顆粒大 = 數量少
但這是「標準」狀況,實際開發時,前端、後端或是特定專案需要的測試比重也會做調整。同時也會伴隨許多階段和突發狀況,因此有個重要守則:
不要在需求不明確的情況下開始構思完整的測試策略。
雖然寫測試是一個良好習慣,但像是 PoC 階段時專案輪廓還沒有成形,會反覆地探索需求、可行性與市場價值,這個時期的需求會快速變動,可能睡醒又全部打掉,這時候去拚測試的覆蓋率意義不大,還是可以寫,但是一樣先圍繞在核心的業務邏輯。