測試的意義

測試的目的是保證程式碼品質的下限,沒有測試的程式碼近乎沒有下限。這樣的程式碼往往也會耗費更多時間來定位 bug,也不容易根除問題。

測試主要用來看某段邏輯是否符合預期,例如:

  • 輸入的 email 符合格式後才進行已註冊 email 的相關驗證
  • 輸入的日期資料沒有符合 'YYYY-MM-DD' 就拋出錯誤
資訊

這個分類的所有文章皆以 TypeScript 搭配 Vitest 示範。


單元測試

單元測試 (Unit Test) 的規模可以是一個函式、類別、模組,也就是針對小規模的邏輯所做的測試。

所以:規模小 = 容易撰寫 = 最快看到收益

單元測試專注在驗證單一功能的各種情境,因此會將連動的外部模組都使用假資料模擬,提高測試的隔離性。

案例:

  • 後端:repository、service 等 CRUD 或資料重組的輸入輸出
  • 前端:UI 元件的狀態變化、hooks 的邏輯

整合測試

整合測試 (Integration Test) 通常需要驗證多個模組的交互流程,假資料的模擬會更少。通常是用來測試一些重要的業務邏輯和模組。

案例:

  • 後端:API 呼叫 → service → repository → 資料庫的完整流程
  • 前端:表單提交 → API 呼叫 → 狀態更新 → UI 渲染

端對端測試

端對端測試 (End to End Test) 會以使用者的角度來驗證完整操作流程。

後端要保證 API 端點的存取流程,前端要模擬使用者的 UI 操作流程。所以嚴格來說,請一個人實際從畫面上操作功能也是一種完整的 E2E 測試(全民公測)。

案例:

  • 後端:註冊 → 登入 → 新增 Todo → 查詢的完整 API 流程
  • 前端:模擬使用者點擊、輸入、導航的完整操作

覆蓋率

覆蓋率 (Coverage) 是指測試的程式碼執行時涵蓋了多少原始的程式碼,常見指標包括:

  • 行覆蓋率:執行過的程式碼行數比例
  • 分支覆蓋率:執行過的條件分支比例 (if/else)
  • 函式覆蓋率:執行過的函式比例
警告

覆蓋率高 ≠ 測試品質好,重點是測試我們真正需要的情境


起手式

測試區塊

輸出兩數總和:

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)。

describeit 的參數結構相同,第一個參數可以輸入文字描述,英文的描述慣例是 should...

測試的運行結果也會將這段描述打印出來:

gh


3A Pattern

輸出陣列中所有數字的總和:

export function sumArray(arr: number[]): number {
  return arr.reduce((acc, curr) => acc + curr, 0);
}

3A 是測試的撰寫慣例:

  1. Arrange(準備): 設定測試資料
  2. Act(執行): 呼叫要測試的函數
  3. Assert(驗證): 檢查結果是否符合預期

測試案例通常都按此結構撰寫:

describe('sumArray 函式', () => {
  it('[1, 2, 3] 應該等於 6', () => {
    // Arrange
    const mockInput = [1, 2, 3];

    // Act
    const result = sumArray(mockInput);

    // Assert
    expect(result).toBe(6);
  });
});
資訊

測試案例需要確保每個案例都要能獨立執行,如果測試之間會互相干擾結果,測試的準確性就不穩定、不真實。


制定測試案例

測試案例不需要窮舉。

測試的目的在於驗證功能行為是否符合預期,而不是在強調每個步驟都要做對,反過來說,先確定這個行為應該造成什麼結果,就會制定出正確的步驟。

例如範例的 sumArray,因為只有一個參數,測試案例只需要:

  • 確認這個函式會回傳一個正確加總過的數字
  • 傳入空陣列會回傳什麼
  • 傳入非陣列的參數會回傳什麼(這會在靜態檢查階段擋下,通常不需要測試這個案例)

小結

gh

標準的測試金字塔會長這樣,這裡可以直覺地去想:

  • 單元 = 顆粒小 = 數量多
  • 端對端 = 顆粒大 = 數量小

但這是「標準」狀況,實際開發會歷經不同的階段和突發狀況,調整各種測試的比例,因此有個重要守則:

不要在需求不明確的情況下開始構思測試策略

雖然「越早導入測試越好」是一個良好習慣,但如果是 PoC 這種專案輪廓還沒有成形的階段,會反覆地探索需求、測試可行性與市場價值,功能需求會快速變動,可能過一晚又推翻了,這時候去拚測試的覆蓋率意義不大。還是可以寫,但是先以核心業務邏輯為主。


參考資料